PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.0
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 / my-wordpress.js

my-wordpress.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.0, at assets/js/my-wordpress.js

8,466 lines 281.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function() {
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 _n(single, plural, number, domain = TEXT_DOMAIN) {
11 return i18n()?._n(single, plural, number, domain) ?? (number === 1 ? single : plural);
12 }
13 function sprintf(format, ...args) {
14 const impl = i18n()?.sprintf;
15 if (impl) {
16 return impl(format, ...args);
17 }
18 let i = 0;
19 return format.replace(/%[sd]/g, () => String(args[i++] ?? ""));
20 }
21 function getWpHooks() {
22 const hooks = window.wp?.hooks;
23 if (!hooks) {
24 throw new Error(
25 "[desktop-mode] `window.wp.hooks` is not available. The plugin declares `wp-hooks` as a script dependency; if you are seeing this error, verify the enqueue order."
26 );
27 }
28 return hooks;
29 }
30 function addAction(hookName2, namespace, callback2, priority) {
31 getWpHooks().addAction(
32 hookName2,
33 namespace,
34 callback2,
35 priority
36 );
37 }
38 function removeAction(hookName2, namespace) {
39 return getWpHooks().removeAction(hookName2, namespace);
40 }
41 function applyFilters(hookName2, value, ...args) {
42 return getWpHooks().applyFilters(hookName2, value, ...args);
43 }
44 function doAction(hookName2, ...args) {
45 getWpHooks().doAction(hookName2, ...args);
46 }
47 const CANARY_TAG = "wpd-confirm-dialog";
48 let inflight = null;
49 function isLoaded() {
50 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
51 }
52 function injectScript(scriptUrl) {
53 return new Promise((resolve, reject) => {
54 const existing = document.querySelector(
55 'script[data-desktop-mode-shell-overlays="1"]'
56 );
57 const finish = () => {
58 if (isLoaded()) {
59 resolve();
60 return;
61 }
62 reject(
63 new Error(
64 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
65 )
66 );
67 };
68 if (existing) {
69 if (isLoaded()) {
70 finish();
71 } else {
72 existing.addEventListener("load", finish);
73 existing.addEventListener(
74 "error",
75 () => reject(new Error("failed to load shell-overlays bundle"))
76 );
77 }
78 return;
79 }
80 const s = document.createElement("script");
81 s.src = scriptUrl;
82 s.async = true;
83 s.dataset.desktopModeShellOverlays = "1";
84 s.addEventListener("load", finish);
85 s.addEventListener(
86 "error",
87 () => reject(new Error("failed to load shell-overlays bundle"))
88 );
89 document.head.appendChild(s);
90 });
91 }
92 function ensureShellOverlaysLoaded(scriptUrl) {
93 if (isLoaded()) {
94 return Promise.resolve();
95 }
96 if (!scriptUrl) {
97 return Promise.resolve();
98 }
99 if (!inflight) {
100 inflight = injectScript(scriptUrl);
101 }
102 return inflight;
103 }
104 function shellOverlaysBundleUrl() {
105 const cfg = window.desktopModeConfig;
106 return cfg?.shellOverlaysBundleUrl ?? "";
107 }
108 function openWithShellOverlays(isStillCurrent, fn) {
109 const url = shellOverlaysBundleUrl();
110 if (isLoaded() || !url) {
111 fn();
112 return;
113 }
114 void ensureShellOverlaysLoaded(url).then(() => {
115 if (!isStillCurrent()) {
116 return;
117 }
118 fn();
119 }).catch((err) => {
120 if (typeof console !== "undefined") {
121 console.warn(
122 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
123 err
124 );
125 }
126 });
127 }
128 const MENU_CLASS = "desktop-mode-icon-canvas-menu";
129 let activeMenu = null;
130 let activeFlyout = null;
131 let activeCanvas = null;
132 let outsideHandler = null;
133 let escHandler = null;
134 function attachIconCanvasMenu(canvas, deps) {
135 deps.openOnBackgroundClick !== false;
136 const onContextMenu = (e) => {
137 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
138 return;
139 }
140 e.preventDefault();
141 toggle(e.clientX, e.clientY);
142 };
143 let toggleGen = 0;
144 const toggle = (x, y) => {
145 if (activeCanvas === canvas && activeMenu) {
146 closeMenu();
147 return;
148 }
149 const items = buildItems(deps);
150 const filtered = applyFilters(
151 "desktop-mode.icon-canvas.menu",
152 items,
153 deps.scope
154 );
155 const finalItems = Array.isArray(filtered) ? filtered : items;
156 const myGen = ++toggleGen;
157 openWithShellOverlays(
158 () => myGen === toggleGen,
159 () => openMenu(finalItems, { x, y }, canvas)
160 );
161 };
162 canvas.addEventListener("contextmenu", onContextMenu);
163 return {
164 dispose: () => {
165 canvas.removeEventListener("contextmenu", onContextMenu);
166 closeMenu();
167 }
168 };
169 }
170 function isInsideTile(target) {
171 if (!(target instanceof Element)) {
172 return false;
173 }
174 return target.closest(".desktop-mode-file-tile") !== null;
175 }
176 function isInsideMenu(target) {
177 if (!(target instanceof Element)) {
178 return false;
179 }
180 return target.closest(`.${MENU_CLASS}`) !== null;
181 }
182 function buildItems(deps) {
183 const sortItem = {
184 id: "sort-by",
185 label: __("Sort by", "desktop-mode"),
186 icon: "dashicons-sort",
187 sort: 10,
188 children: [
189 {
190 id: "sort-name-asc",
191 label: __("Name (A → Z)", "desktop-mode"),
192 sort: 10,
193 onClick: () => deps.onSort("name-asc")
194 },
195 {
196 id: "sort-name-desc",
197 label: __("Name (Z → A)", "desktop-mode"),
198 sort: 20,
199 onClick: () => deps.onSort("name-desc")
200 },
201 {
202 id: "sort-date-desc",
203 label: __("Newest first", "desktop-mode"),
204 sort: 30,
205 onClick: () => deps.onSort("date-desc")
206 },
207 {
208 id: "sort-date-asc",
209 label: __("Oldest first", "desktop-mode"),
210 sort: 40,
211 onClick: () => deps.onSort("date-asc")
212 }
213 ]
214 };
215 const items = [sortItem];
216 if (Array.isArray(deps.extraItems)) {
217 items.push(...deps.extraItems);
218 }
219 return items;
220 }
221 function sortItems(items) {
222 return items.slice().sort((a, b) => {
223 const sa = typeof a.sort === "number" ? a.sort : 100;
224 const sb = typeof b.sort === "number" ? b.sort : 100;
225 if (sa !== sb) {
226 return sa - sb;
227 }
228 return a.label.localeCompare(b.label);
229 });
230 }
231 function openMenu(items, pos, canvas) {
232 closeMenu();
233 if (items.length === 0) {
234 return;
235 }
236 activeCanvas = canvas;
237 const sorted = sortItems(items);
238 const menu = document.createElement("wpd-context-menu");
239 menu.setAttribute("open", "");
240 menu.classList.add(MENU_CLASS);
241 menu.style.left = `${pos.x}px`;
242 menu.style.top = `${pos.y}px`;
243 const itemById = /* @__PURE__ */ new Map();
244 for (const item of sorted) {
245 itemById.set(item.id, item);
246 const opt = appendOption(menu, item);
247 if (hasChildren(item)) {
248 opt.addEventListener("mouseenter", () => {
249 openFlyout(item, opt);
250 });
251 }
252 }
253 menu.addEventListener("wpd-context-menu-pick", (e) => {
254 const detail = e.detail;
255 const item = itemById.get(detail.id);
256 if (!item) {
257 return;
258 }
259 if (hasChildren(item)) {
260 e.stopPropagation();
261 const anchor = menu.querySelector(
262 `[data-menu-item-id="${item.id}"]`
263 );
264 if (anchor) {
265 openFlyout(item, anchor);
266 }
267 return;
268 }
269 closeMenu();
270 item.onClick?.();
271 });
272 document.body.appendChild(menu);
273 activeMenu = menu;
274 clampToViewport(menu);
275 queueMicrotask(() => {
276 outsideHandler = (e) => {
277 if (isInsideMenu(e.target)) {
278 return;
279 }
280 closeMenu();
281 };
282 escHandler = (e) => {
283 if (e.key === "Escape") {
284 closeMenu();
285 }
286 };
287 document.addEventListener("mousedown", outsideHandler);
288 document.addEventListener("keydown", escHandler);
289 });
290 }
291 function appendOption(host, item) {
292 const opt = document.createElement("wpd-context-menu-option");
293 opt.dataset.menuItemId = item.id;
294 opt.setAttribute("value", item.id);
295 if (item.heading) {
296 opt.setAttribute("heading", "");
297 }
298 if (item.disabled) {
299 opt.setAttribute("disabled", "");
300 }
301 if (item.icon) {
302 opt.setAttribute("icon", sanitizeClass$1(item.icon));
303 }
304 if (hasChildren(item)) {
305 opt.setAttribute("has-children", "");
306 }
307 opt.textContent = item.label;
308 host.appendChild(opt);
309 return opt;
310 }
311 function openFlyout(parent, anchor) {
312 closeFlyout();
313 if (!hasChildren(parent)) {
314 return;
315 }
316 const fly = document.createElement("wpd-context-menu");
317 fly.setAttribute("open", "");
318 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
319 const childById = /* @__PURE__ */ new Map();
320 for (const child of sortItems(parent.children ?? [])) {
321 childById.set(child.id, child);
322 appendOption(fly, child);
323 }
324 fly.addEventListener("wpd-context-menu-pick", (e) => {
325 const detail = e.detail;
326 const child = childById.get(detail.id);
327 if (!child) {
328 return;
329 }
330 e.stopPropagation();
331 closeMenu();
332 child.onClick?.();
333 });
334 document.body.appendChild(fly);
335 activeFlyout = fly;
336 positionFlyout(fly, anchor);
337 }
338 function positionFlyout(fly, anchor) {
339 const ar = anchor.getBoundingClientRect();
340 fly.style.position = "fixed";
341 fly.style.left = `${ar.right}px`;
342 fly.style.top = `${ar.top}px`;
343 const fr = fly.getBoundingClientRect();
344 if (fr.right > window.innerWidth) {
345 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
346 }
347 if (fr.bottom > window.innerHeight) {
348 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
349 }
350 }
351 function clampToViewport(menu) {
352 const rect = menu.getBoundingClientRect();
353 if (rect.right > window.innerWidth) {
354 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
355 }
356 if (rect.bottom > window.innerHeight) {
357 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
358 }
359 }
360 function hasChildren(item) {
361 return Array.isArray(item.children) && item.children.length > 0;
362 }
363 function closeFlyout() {
364 if (activeFlyout) {
365 activeFlyout.remove();
366 activeFlyout = null;
367 }
368 }
369 function closeMenu() {
370 closeFlyout();
371 if (activeMenu) {
372 activeMenu.remove();
373 activeMenu = null;
374 }
375 activeCanvas = null;
376 if (outsideHandler) {
377 document.removeEventListener("mousedown", outsideHandler);
378 outsideHandler = null;
379 }
380 if (escHandler) {
381 document.removeEventListener("keydown", escHandler);
382 escHandler = null;
383 }
384 }
385 function sanitizeClass$1(raw) {
386 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
387 }
388 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
389 const ROOT_CLASS$1 = STATUS_BAR_CLASS;
390 function renderStatusBarSegments(bar, segments) {
391 render$1(bar, segments);
392 }
393 function render$1(bar, segments) {
394 const sort = (a, b) => {
395 const sa = typeof a.sort === "number" ? a.sort : 100;
396 const sb = typeof b.sort === "number" ? b.sort : 100;
397 if (sa !== sb) {
398 return sa - sb;
399 }
400 return a.label.localeCompare(b.label);
401 };
402 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
403 const end = segments.filter((s) => s.align === "end").sort(sort);
404 bar.replaceChildren();
405 bar.appendChild(buildCluster("start", start));
406 bar.appendChild(buildCluster("end", end));
407 }
408 function buildCluster(align, segs) {
409 const cluster = document.createElement("div");
410 cluster.className = `${ROOT_CLASS$1}__cluster ${ROOT_CLASS$1}__cluster--${align}`;
411 for (const seg of segs) {
412 cluster.appendChild(buildSegment(seg));
413 }
414 return cluster;
415 }
416 function buildSegment(seg) {
417 const interactive = typeof seg.onClick === "function";
418 const el = document.createElement(interactive ? "button" : "span");
419 el.className = `${ROOT_CLASS$1}__segment`;
420 el.dataset.segmentId = seg.id;
421 if (interactive) {
422 el.type = "button";
423 el.addEventListener("click", (e) => seg.onClick(e));
424 }
425 if (seg.icon) {
426 const icon = document.createElement("span");
427 icon.className = `${ROOT_CLASS$1}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
428 icon.setAttribute("aria-hidden", "true");
429 el.appendChild(icon);
430 }
431 const label = document.createElement("span");
432 label.className = `${ROOT_CLASS$1}__label`;
433 label.textContent = seg.label;
434 el.appendChild(label);
435 return el;
436 }
437 function html(strings, ...values) {
438 return { __wpdHtml: true, strings, values };
439 }
440 function isTemplateResult(v) {
441 return !!v && v.__wpdHtml === true;
442 }
443 const MARKER_PREFIX = "$$wpd$$";
444 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
445 function joinWithMarkers(strings) {
446 let out = strings[0];
447 for (let i = 1; i < strings.length; i++) {
448 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
449 }
450 return out;
451 }
452 const compiledCache = /* @__PURE__ */ new WeakMap();
453 function compile(strings) {
454 const cached = compiledCache.get(strings);
455 if (cached) {
456 return cached;
457 }
458 const template = document.createElement("template");
459 template.innerHTML = joinWithMarkers(strings);
460 const recipes = [];
461 const walk = (node, path) => {
462 if (node.nodeType === Node.ELEMENT_NODE) {
463 const el = node;
464 for (const attr of Array.from(el.attributes)) {
465 const rawName = attr.name;
466 const rawValue = attr.value;
467 const prefix = rawName[0];
468 if (MARKER_RE.test(rawValue)) {
469 MARKER_RE.lastIndex = 0;
470 if (prefix === "@") {
471 const match = MARKER_RE.exec(rawValue);
472 MARKER_RE.lastIndex = 0;
473 recipes.push({
474 path,
475 kind: "event",
476 name: rawName.slice(1),
477 valueIndex: match ? Number(match[1]) : 0
478 });
479 el.removeAttribute(rawName);
480 } else if (prefix === ".") {
481 const match = MARKER_RE.exec(rawValue);
482 MARKER_RE.lastIndex = 0;
483 recipes.push({
484 path,
485 kind: "prop",
486 name: rawName.slice(1),
487 valueIndex: match ? Number(match[1]) : 0
488 });
489 el.removeAttribute(rawName);
490 } else if (prefix === "?") {
491 const match = MARKER_RE.exec(rawValue);
492 MARKER_RE.lastIndex = 0;
493 recipes.push({
494 path,
495 kind: "bool",
496 name: rawName.slice(1),
497 valueIndex: match ? Number(match[1]) : 0
498 });
499 el.removeAttribute(rawName);
500 } else {
501 const fragments = [];
502 const indices = [];
503 let lastEnd = 0;
504 let m;
505 MARKER_RE.lastIndex = 0;
506 while ((m = MARKER_RE.exec(rawValue)) !== null) {
507 fragments.push(rawValue.slice(lastEnd, m.index));
508 indices.push(Number(m[1]));
509 lastEnd = m.index + m[0].length;
510 }
511 fragments.push(rawValue.slice(lastEnd));
512 recipes.push({
513 path,
514 kind: "attr",
515 name: rawName,
516 template: fragments,
517 valueIndices: indices
518 });
519 el.setAttribute(rawName, "");
520 }
521 }
522 }
523 }
524 const children = Array.from(node.childNodes);
525 let shift = 0;
526 for (let i = 0; i < children.length; i++) {
527 const child = children[i];
528 const liveIndex = i + shift;
529 if (child.nodeType === Node.TEXT_NODE) {
530 const text = child.textContent || "";
531 if (!MARKER_RE.test(text)) {
532 MARKER_RE.lastIndex = 0;
533 continue;
534 }
535 MARKER_RE.lastIndex = 0;
536 const parent = child.parentNode;
537 let lastEnd = 0;
538 let m;
539 const newNodes = [];
540 const newRecipes = [];
541 MARKER_RE.lastIndex = 0;
542 while ((m = MARKER_RE.exec(text)) !== null) {
543 if (m.index > lastEnd) {
544 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
545 }
546 const placeholder = document.createTextNode("");
547 newNodes.push(placeholder);
548 newRecipes.push({
549 path: [...path, liveIndex + newNodes.length - 1],
550 kind: "node",
551 valueIndex: Number(m[1])
552 });
553 lastEnd = m.index + m[0].length;
554 }
555 if (lastEnd < text.length) {
556 newNodes.push(document.createTextNode(text.slice(lastEnd)));
557 }
558 for (const nn of newNodes) {
559 parent.insertBefore(nn, child);
560 }
561 parent.removeChild(child);
562 shift += newNodes.length - 1;
563 recipes.push(...newRecipes);
564 } else {
565 walk(child, [...path, liveIndex]);
566 }
567 }
568 };
569 walk(template.content, []);
570 const buildParts = (fragment) => {
571 const out = [];
572 for (const r of recipes) {
573 let node = fragment;
574 for (const idx of r.path) {
575 node = node.childNodes[idx];
576 }
577 if (r.kind === "node") {
578 out.push({
579 kind: "node",
580 valueIndex: r.valueIndex,
581 child: {
582 anchor: node,
583 state: null
584 }
585 });
586 } else if (r.kind === "attr") {
587 out.push({
588 kind: "attr",
589 element: node,
590 name: r.name,
591 template: r.template,
592 valueIndices: r.valueIndices
593 });
594 } else if (r.kind === "event") {
595 out.push({
596 kind: "event",
597 valueIndex: r.valueIndex,
598 element: node,
599 name: r.name
600 });
601 } else if (r.kind === "prop") {
602 out.push({
603 kind: "prop",
604 valueIndex: r.valueIndex,
605 element: node,
606 name: r.name
607 });
608 } else if (r.kind === "bool") {
609 out.push({
610 kind: "bool",
611 valueIndex: r.valueIndex,
612 element: node,
613 name: r.name
614 });
615 }
616 }
617 return out;
618 };
619 const entry = { template, buildParts };
620 compiledCache.set(strings, entry);
621 return entry;
622 }
623 const mountState = /* @__PURE__ */ new WeakMap();
624 function render(result, container) {
625 const existing = mountState.get(container);
626 if (existing && existing.strings === result.strings) {
627 applyValues(existing.parts, result.values);
628 return;
629 }
630 const compiled = compile(result.strings);
631 const fragment = compiled.template.content.cloneNode(true);
632 const parts = compiled.buildParts(fragment);
633 while (container.firstChild) {
634 container.removeChild(container.firstChild);
635 }
636 container.appendChild(fragment);
637 applyValues(parts, result.values);
638 mountState.set(container, { strings: result.strings, parts });
639 }
640 function applyValues(parts, values) {
641 for (const part of parts) {
642 if (part.kind === "node") {
643 updateChildPart(part.child, values[part.valueIndex]);
644 } else if (part.kind === "attr") {
645 let composed = part.template[0];
646 for (let i = 0; i < part.valueIndices.length; i++) {
647 composed += formatText(values[part.valueIndices[i]]);
648 composed += part.template[i + 1];
649 }
650 if (composed !== part.last) {
651 part.last = composed;
652 if (composed === "") {
653 part.element.removeAttribute(part.name);
654 } else {
655 part.element.setAttribute(part.name, composed);
656 }
657 }
658 } else if (part.kind === "event") {
659 const next = values[part.valueIndex];
660 if (next !== part.current) {
661 if (part.current) {
662 part.element.removeEventListener(part.name, part.current);
663 }
664 if (next) {
665 part.element.addEventListener(part.name, next);
666 }
667 part.current = next;
668 }
669 } else if (part.kind === "prop") {
670 const next = values[part.valueIndex];
671 if (next !== part.last) {
672 part.last = next;
673 part.element[part.name] = next;
674 }
675 } else if (part.kind === "bool") {
676 const next = !!values[part.valueIndex];
677 if (next !== part.last) {
678 part.last = next;
679 if (next) {
680 part.element.setAttribute(part.name, "");
681 } else {
682 part.element.removeAttribute(part.name);
683 }
684 }
685 }
686 }
687 }
688 function updateChildPart(child, value) {
689 if (value === null || value === void 0 || value === false) {
690 if (child.state) {
691 disposeChildState(child.state);
692 child.state = null;
693 }
694 return;
695 }
696 if (Array.isArray(value)) {
697 updateArrayChild(child, value);
698 return;
699 }
700 if (isTemplateResult(value)) {
701 updateTemplateChild(child, value);
702 return;
703 }
704 if (value instanceof Node) {
705 updateNodeChild(child, value);
706 return;
707 }
708 updateTextChild(child, formatText(value));
709 }
710 function updateNodeChild(child, node) {
711 const old = child.state;
712 if (old?.shape === "node" && old.node === node) {
713 return;
714 }
715 if (old) {
716 disposeChildState(old);
717 }
718 insertBeforeAnchor(child, [node]);
719 child.state = { shape: "node", node };
720 }
721 function updateTextChild(child, text) {
722 const old = child.state;
723 if (old?.shape === "text") {
724 if (old.text !== text) {
725 old.node.textContent = text;
726 old.text = text;
727 }
728 return;
729 }
730 if (old) {
731 disposeChildState(old);
732 }
733 const node = document.createTextNode(text);
734 insertBeforeAnchor(child, [node]);
735 child.state = { shape: "text", node, text };
736 }
737 function updateTemplateChild(child, result) {
738 const old = child.state;
739 if (old?.shape === "template" && old.strings === result.strings) {
740 applyValues(old.parts, result.values);
741 return;
742 }
743 if (old) {
744 disposeChildState(old);
745 }
746 const compiled = compile(result.strings);
747 const fragment = compiled.template.content.cloneNode(true);
748 const parts = compiled.buildParts(fragment);
749 const topNodes = Array.from(fragment.childNodes);
750 insertBeforeAnchor(child, [fragment]);
751 applyValues(parts, result.values);
752 child.state = {
753 shape: "template",
754 strings: result.strings,
755 parts,
756 nodes: topNodes
757 };
758 }
759 function updateArrayChild(child, arr) {
760 const old = child.state;
761 if (old?.shape === "array" && old.entries.length === arr.length) {
762 for (let i = 0; i < arr.length; i++) {
763 updateChildPart(old.entries[i], arr[i]);
764 }
765 return;
766 }
767 if (old) {
768 disposeChildState(old);
769 }
770 const entries = [];
771 for (const v of arr) {
772 const entryAnchor = document.createTextNode("");
773 insertBeforeAnchor(child, [entryAnchor]);
774 const entry = { anchor: entryAnchor, state: null };
775 updateChildPart(entry, v);
776 entries.push(entry);
777 }
778 child.state = { shape: "array", entries };
779 }
780 function insertBeforeAnchor(child, nodes) {
781 const parent = child.anchor.parentNode;
782 if (!parent) {
783 return;
784 }
785 for (const node of nodes) {
786 parent.insertBefore(node, child.anchor);
787 }
788 }
789 function disposeChildState(state) {
790 if (state.shape === "text") {
791 state.node.remove();
792 return;
793 }
794 if (state.shape === "template") {
795 for (const node of state.nodes) {
796 if (node.parentNode) {
797 node.parentNode.removeChild(node);
798 }
799 }
800 return;
801 }
802 if (state.shape === "node") {
803 if (state.node.parentNode) {
804 state.node.parentNode.removeChild(state.node);
805 }
806 return;
807 }
808 for (const entry of state.entries) {
809 if (entry.state) {
810 disposeChildState(entry.state);
811 }
812 entry.anchor.remove();
813 }
814 }
815 function formatText(v) {
816 if (v === null || v === void 0 || v === false) {
817 return "";
818 }
819 return String(v);
820 }
821 const _Component = class _Component extends HTMLElement {
822 constructor() {
823 super();
824 this._renderScheduled = false;
825 this._propValues = {};
826 const ctor = this.constructor;
827 if (ctor.shadow) {
828 this.attachShadow({ mode: "open" });
829 this._renderRoot = this.shadowRoot;
830 } else {
831 this._renderRoot = this;
832 }
833 this._installPropAccessors();
834 }
835 static get observedAttributes() {
836 return this.props.map(kebab);
837 }
838 connectedCallback() {
839 this._adoptStyles();
840 this.requestUpdate();
841 }
842 attributeChangedCallback(name, oldValue, newValue) {
843 if (oldValue === newValue) {
844 return;
845 }
846 const prop = camel(name);
847 this._propValues[prop] = newValue;
848 this.requestUpdate();
849 }
850 /**
851 * Declarative class-name setter. Assign an array (or a
852 * space-separated string) and the host's `class` attribute is
853 * rewritten to match. Intended for programmatic styling — when
854 * a plugin has enqueued its own stylesheet and wants to apply
855 * one of those classes to a shell component:
856 *
857 * ```js
858 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
859 * // → <wpd-select class="my-plugin-brand is-active">
860 * ```
861 *
862 * The plain HTML `class="…"` attribute works just the same and
863 * is always preferred when writing markup by hand — this setter
864 * exists for the JS-API case where the caller has an array of
865 * conditional classes in hand.
866 *
867 * Getter returns the current `classList` as a plain array for
868 * symmetric read/write.
869 *
870 * @since 0.13.0
871 */
872 get classNames() {
873 return Array.from(this.classList);
874 }
875 set classNames(next) {
876 if (next === null || next === void 0) {
877 this.removeAttribute("class");
878 return;
879 }
880 const list = Array.isArray(next) ? next : String(next).split(/\s+/);
881 const cleaned = list.map((s) => String(s).trim()).filter((s) => s !== "");
882 this.className = cleaned.join(" ");
883 }
884 /**
885 * Request a re-render explicitly. Components rarely need this —
886 * declare state via props + attribute observers and the render
887 * loop picks up changes automatically.
888 */
889 requestUpdate() {
890 this._scheduleRender();
891 }
892 /**
893 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
894 * by default (matches typical WC UX — events cross shadow
895 * boundaries, parents can listen without knowing about internal
896 * structure).
897 */
898 emit(name, detail) {
899 return this.dispatchEvent(
900 new CustomEvent(name, {
901 detail,
902 bubbles: true,
903 composed: true
904 })
905 );
906 }
907 // ------------------------------------------------------------------
908 // Internals
909 // ------------------------------------------------------------------
910 /**
911 * Wire every `static props` entry to a matched property getter +
912 * setter on the element. Setting the property reflects into the
913 * attribute (so downstream observers + CSS selectors see it);
914 * reading the property falls back to the attribute.
915 */
916 _installPropAccessors() {
917 const ctor = this.constructor;
918 for (const prop of ctor.props) {
919 if (Object.getOwnPropertyDescriptor(this, prop)) {
920 continue;
921 }
922 const attr = kebab(prop);
923 Object.defineProperty(this, prop, {
924 get: () => {
925 if (prop in this._propValues) {
926 return this._propValues[prop];
927 }
928 return this.getAttribute(attr);
929 },
930 set: (value) => {
931 let str;
932 if (value === null || value === void 0 || value === false) {
933 str = null;
934 } else if (value === true) {
935 str = "";
936 } else {
937 str = String(value);
938 }
939 this._propValues[prop] = str;
940 if (str === null) {
941 this.removeAttribute(attr);
942 } else {
943 this.setAttribute(attr, str);
944 }
945 this.requestUpdate();
946 },
947 enumerable: true,
948 configurable: true
949 });
950 }
951 }
952 /**
953 * Schedule a render on the next microtask. Multiple property
954 * assignments in the same tick collapse into a single render.
955 */
956 _scheduleRender() {
957 if (this._renderScheduled || !this.isConnected) {
958 return;
959 }
960 this._renderScheduled = true;
961 queueMicrotask(() => {
962 this._renderScheduled = false;
963 if (!this.isConnected) {
964 return;
965 }
966 render(this.render(), this._renderRoot);
967 });
968 }
969 /**
970 * Mount adoptable stylesheets onto the shadow root (via
971 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
972 * tag per def). No-op if `static styles` is empty.
973 */
974 _adoptStyles() {
975 const ctor = this.constructor;
976 if (ctor.styles.length === 0) {
977 return;
978 }
979 if (ctor.shadow && this.shadowRoot) {
980 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
981 this.shadowRoot.adoptedStyleSheets = sheets;
982 if (sheets.length !== ctor.styles.length) {
983 for (const s of ctor.styles) {
984 if (!s.sheet) {
985 const tag = document.createElement("style");
986 tag.textContent = s.cssText;
987 this.shadowRoot.appendChild(tag);
988 }
989 }
990 }
991 } else {
992 this._adoptLightStyles(ctor);
993 }
994 }
995 _adoptLightStyles(ctor) {
996 if (_Component._lightStylesAdopted.has(ctor)) {
997 return;
998 }
999 _Component._lightStylesAdopted.add(ctor);
1000 for (const s of ctor.styles) {
1001 const tag = document.createElement("style");
1002 tag.dataset.wpdUi = this.tagName.toLowerCase();
1003 tag.textContent = s.cssText;
1004 document.head.appendChild(tag);
1005 }
1006 }
1007 };
1008 _Component.props = [];
1009 _Component.styles = [];
1010 _Component.shadow = true;
1011 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
1012 let Component = _Component;
1013 function defineComponent(tag, ctor) {
1014 if (customElements.get(tag)) {
1015 return;
1016 }
1017 customElements.define(tag, ctor);
1018 }
1019 function kebab(s) {
1020 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
1021 }
1022 function camel(s) {
1023 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1024 }
1025 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
1026 try {
1027 const s = new CSSStyleSheet();
1028 return typeof s.replaceSync === "function";
1029 } catch {
1030 return false;
1031 }
1032 })();
1033 function css(strings, ...values) {
1034 let text = strings[0];
1035 for (let i = 1; i < strings.length; i++) {
1036 const v = values[i - 1];
1037 if (typeof v === "string" || typeof v === "number") {
1038 text += String(v);
1039 } else if (v && v.__wpdCss) {
1040 text += v.cssText;
1041 } else {
1042 throw new TypeError(
1043 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
1044 );
1045 }
1046 text += strings[i];
1047 }
1048 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
1049 const sheet = new CSSStyleSheet();
1050 sheet.replaceSync(text);
1051 return { __wpdCss: true, sheet, cssText: text };
1052 }
1053 return { __wpdCss: true, sheet: null, cssText: text };
1054 }
1055 function hashTitleToHue(input) {
1056 if (!input) {
1057 return 214;
1058 }
1059 let hash = 5381;
1060 for (let i = 0; i < input.length; i++) {
1061 hash = Math.imul(hash, 33) + input.charCodeAt(i);
1062 }
1063 return (hash % 360 + 360) % 360;
1064 }
1065 function renderIcon(icon, opts) {
1066 const className = opts.className ?? "";
1067 const title = opts.title ?? "";
1068 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
1069 const el = document.createElement("span");
1070 el.className = `dashicons ${icon} ${className}`.trim();
1071 el.setAttribute("aria-hidden", "true");
1072 return el;
1073 }
1074 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
1075 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
1076 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
1077 const el = document.createElement("span");
1078 el.className = className;
1079 el.setAttribute("aria-hidden", "true");
1080 el.style.backgroundImage = `url("${icon}")`;
1081 el.style.backgroundRepeat = "no-repeat";
1082 el.style.backgroundPosition = "center";
1083 el.style.backgroundSize = "contain";
1084 el.style.display = "inline-block";
1085 return el;
1086 }
1087 }
1088 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
1089 const commaIdx = icon.indexOf(",");
1090 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
1091 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
1092 return makeImgIcon(icon, className);
1093 }
1094 }
1095 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
1096 return makeImgIcon(icon, className);
1097 }
1098 const span = document.createElement("span");
1099 span.className = `${className} desktop-mode-icon-letter`.trim();
1100 span.setAttribute("aria-hidden", "true");
1101 const letters = letterFromTitle(title);
1102 span.textContent = letters;
1103 const hue = hashTitleToHue(title);
1104 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
1105 span.style.color = "#fff";
1106 span.style.display = "inline-flex";
1107 span.style.alignItems = "center";
1108 span.style.justifyContent = "center";
1109 span.style.fontWeight = "600";
1110 span.style.borderRadius = "4px";
1111 return span;
1112 }
1113 function makeImgIcon(src, className) {
1114 const img = document.createElement("img");
1115 img.className = className;
1116 img.src = src;
1117 img.alt = "";
1118 img.setAttribute("aria-hidden", "true");
1119 img.draggable = false;
1120 return img;
1121 }
1122 function letterFromTitle(title) {
1123 const trimmed = (title ?? "").trim();
1124 if (trimmed === "") {
1125 return "?";
1126 }
1127 const words = trimmed.split(/\s+/);
1128 if (words.length >= 2) {
1129 return (words[0][0] + words[1][0]).toUpperCase();
1130 }
1131 const first = words[0];
1132 if (first.length >= 2) {
1133 return first.slice(0, 2).toUpperCase();
1134 }
1135 return first.toUpperCase();
1136 }
1137 function applyTileEntryStagger(tile) {
1138 tile.style.setProperty(
1139 "--desktop-mode-file-tile-enter-delay",
1140 `${(Math.random() * 0.25).toFixed(3)}s`
1141 );
1142 tile.style.setProperty(
1143 "--desktop-mode-file-tile-enter-duration",
1144 `${(0.3 + Math.random() * 0.25).toFixed(3)}s`
1145 );
1146 }
1147 const styles$3 = css`:host{display:inline-block}`;
1148 const styles$2 = css`:host{position:absolute;width:var( --wpd-ribbon-size,90px );height:var( --wpd-ribbon-size,90px );overflow:hidden;pointer-events:none;z-index:var( --wpd-ribbon-z,2 )}:host( [ hidden ] ){display:none}.banner{position:absolute;display:block;width:var( --wpd-ribbon-banner-width,140px );padding:var( --wpd-ribbon-padding,4px 0 );text-align:center;font:var( --wpd-ribbon-font,700 10px/1.4 var( --desktop-mode-font,system-ui ) );letter-spacing:var( --wpd-ribbon-tracking,0.06em );text-transform:uppercase;color:var( --wpd-ribbon-fg,#fff );background:var( --wpd-ribbon-bg,var( --wp-admin-theme-color,#2271b1 ) );box-shadow:var( --wpd-ribbon-shadow,0 2px 4px rgba( 0,0,0,0.2 ) )}:host(:not( [ placement ] ) ),:host( [ placement='top-end' ] ){inset-block-start:0;inset-inline-end:0}:host(:not( [ placement ] ) ) .banner,:host( [ placement='top-end' ] ) .banner{inset-block-start:var( --wpd-ribbon-banner-offset,20px );inset-inline-end:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( 45deg )}:host( [ placement='top-start' ] ){inset-block-start:0;inset-inline-start:0}:host( [ placement='top-start' ] ) .banner{inset-block-start:var( --wpd-ribbon-banner-offset,20px );inset-inline-start:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( -45deg )}:host( [ placement='bottom-end' ] ){inset-block-end:0;inset-inline-end:0}:host( [ placement='bottom-end' ] ) .banner{inset-block-end:var( --wpd-ribbon-banner-offset,20px );inset-inline-end:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( -45deg )}:host( [ placement='bottom-start' ] ){inset-block-end:0;inset-inline-start:0}:host( [ placement='bottom-start' ] ) .banner{inset-block-end:var( --wpd-ribbon-banner-offset,20px );inset-inline-start:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host(:not( [ placement ] ) ) .banner,:host-context( [ dir='rtl' ] ):host( [ placement='top-end' ] ) .banner{transform:rotate( -45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='top-start' ] ) .banner{transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='bottom-end' ] ) .banner{transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='bottom-start' ] ) .banner{transform:rotate( -45deg )}:host( [ tone='success' ] ) .banner{background:var( --wpd-ribbon-success,#1a7f37 )}:host( [ tone='warning' ] ) .banner{background:var( --wpd-ribbon-warning,#9a6700 )}:host( [ tone='danger' ] ) .banner{background:var( --wpd-ribbon-danger,#cf222e )}:host( [ tone='info' ] ) .banner{background:var( --wpd-ribbon-info,#0969da )}:host( [ tone='neutral' ] ) .banner{background:var( --wpd-ribbon-neutral,#57606a )}`;
1149 const _WpdRibbon = class _WpdRibbon extends Component {
1150 render() {
1151 return html`<span class="banner" part="banner"><slot></slot></span>`;
1152 }
1153 };
1154 _WpdRibbon.props = ["placement", "tone"];
1155 _WpdRibbon.styles = [styles$2];
1156 _WpdRibbon.help = {
1157 title: "Ribbon",
1158 summary: "45° corner ribbon. Wraps the top-end (default), top-start, bottom-end, or bottom-start corner of its positioned parent. The host owns clipping + rotation; consumers only set position-relative on the parent and drop a label inside.",
1159 status: "experimental",
1160 since: "0.20.0",
1161 props: [
1162 {
1163 name: "placement",
1164 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
1165 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
1166 },
1167 {
1168 name: "tone",
1169 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
1170 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
1171 }
1172 ],
1173 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
1174 cssProps: [
1175 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
1176 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
1177 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
1178 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
1179 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
1180 { name: "--wpd-ribbon-fg", default: "#fff" },
1181 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
1182 { name: "--wpd-ribbon-padding", default: "4px 0" },
1183 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
1184 { name: "--wpd-ribbon-tracking", default: "0.06em" },
1185 { name: "--wpd-ribbon-z", default: "2" }
1186 ],
1187 example: html`
1188 <div
1189 style="position: relative; width: 240px; height: 120px;
1190 border: 1px solid #ccc; border-radius: 8px;
1191 padding: 16px; box-sizing: border-box;"
1192 >
1193 <wpd-ribbon>Featured</wpd-ribbon>
1194 Card body…
1195 </div>
1196 `
1197 };
1198 let WpdRibbon = _WpdRibbon;
1199 defineComponent("wpd-ribbon", WpdRibbon);
1200 const TILE_CLASS = "desktop-mode-file-tile";
1201 const STATUS_LABEL = {
1202 draft: "Draft",
1203 pending: "Pending",
1204 private: "Private",
1205 future: "Scheduled"
1206 };
1207 function statusRibbonsEnabled() {
1208 const get = window.wp?.desktop?.getOsSettings;
1209 if (typeof get !== "function") {
1210 return true;
1211 }
1212 try {
1213 return get()?.showPostStatusRibbons !== false;
1214 } catch {
1215 return true;
1216 }
1217 }
1218 function getDragManager$1() {
1219 const api = window.wp?.desktop?.dragManager;
1220 return api ?? null;
1221 }
1222 const REACTIVE_PROPS = [
1223 "type",
1224 "ref",
1225 "label",
1226 "icon",
1227 "thumbnail",
1228 "kind",
1229 "status",
1230 "selected",
1231 "missing",
1232 "access-gated",
1233 "drag-kind",
1234 "drag-title",
1235 "drag-icon"
1236 ];
1237 const _WpdTile = class _WpdTile extends Component {
1238 constructor() {
1239 super(...arguments);
1240 this._pointerdownHandler = null;
1241 this._keydownHandler = null;
1242 }
1243 connectedCallback() {
1244 super.connectedCallback();
1245 if (!this._keydownHandler) {
1246 this._keydownHandler = (e) => {
1247 if (e.key === "Enter" || e.key === " ") {
1248 e.preventDefault();
1249 this.click();
1250 }
1251 };
1252 this.addEventListener("keydown", this._keydownHandler);
1253 }
1254 this._paint();
1255 }
1256 disconnectedCallback() {
1257 if (this._pointerdownHandler) {
1258 this.removeEventListener(
1259 "pointerdown",
1260 this._pointerdownHandler
1261 );
1262 this._pointerdownHandler = null;
1263 }
1264 if (this._keydownHandler) {
1265 this.removeEventListener(
1266 "keydown",
1267 this._keydownHandler
1268 );
1269 this._keydownHandler = null;
1270 }
1271 }
1272 /**
1273 * Bypass the templated render loop. Lit-html's `render(template,
1274 * root)` would wipe the host's light-DOM children every tick —
1275 * including the visual / label / ribbon `_paint()` just
1276 * inserted. We override `requestUpdate` directly so attribute
1277 * changes call `_paint` (idempotent) without lit-html getting
1278 * involved.
1279 */
1280 requestUpdate() {
1281 if (!this.isConnected) {
1282 return;
1283 }
1284 this._paint();
1285 }
1286 render() {
1287 return html``;
1288 }
1289 _paint() {
1290 const type = this.getAttribute("type") ?? "";
1291 const ref = this.getAttribute("ref") ?? "";
1292 const label = this.getAttribute("label") ?? "";
1293 const icon = this.getAttribute("icon") ?? "";
1294 const thumbnail = this.getAttribute("thumbnail") ?? "";
1295 const kind = this.getAttribute("kind") ?? "entry";
1296 const status = this.getAttribute("status") ?? "";
1297 const selected = this.hasAttribute("selected");
1298 const missing = this.hasAttribute("missing");
1299 const accessGated = this.hasAttribute("access-gated");
1300 const ownedClasses = [
1301 TILE_CLASS,
1302 `${TILE_CLASS}--folder`,
1303 `${TILE_CLASS}--missing`,
1304 `${TILE_CLASS}--access-gated`,
1305 `${TILE_CLASS}--selected`
1306 ];
1307 for (const c of ownedClasses) {
1308 this.classList.remove(c);
1309 }
1310 this.classList.add(TILE_CLASS);
1311 if (kind === "folder") {
1312 this.classList.add(`${TILE_CLASS}--folder`);
1313 }
1314 if (missing) {
1315 this.classList.add(`${TILE_CLASS}--missing`);
1316 }
1317 if (accessGated) {
1318 this.classList.add(`${TILE_CLASS}--access-gated`);
1319 }
1320 if (selected) {
1321 this.classList.add(`${TILE_CLASS}--selected`);
1322 }
1323 this.dataset.fileType = type;
1324 this.dataset.fileRef = ref;
1325 if (kind) {
1326 this.dataset.role = kind;
1327 }
1328 this.setAttribute("role", "listitem");
1329 this.setAttribute("aria-label", label);
1330 if (!this.hasAttribute("tabindex")) {
1331 this.setAttribute("tabindex", "0");
1332 }
1333 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
1334 if (accessGated) {
1335 this.title = accessGatedTitle;
1336 this.setAttribute("aria-disabled", "true");
1337 } else {
1338 this.removeAttribute("aria-disabled");
1339 if (this.title === accessGatedTitle) {
1340 this.removeAttribute("title");
1341 }
1342 }
1343 const SLOTS = [
1344 `${TILE_CLASS}__visual`,
1345 `${TILE_CLASS}__label`,
1346 `${TILE_CLASS}__lock`
1347 ];
1348 for (const cls of SLOTS) {
1349 this.querySelectorAll(`:scope > .${cls}`).forEach(
1350 (n) => n.remove()
1351 );
1352 }
1353 this.querySelectorAll(":scope > wpd-ribbon").forEach(
1354 (n) => n.remove()
1355 );
1356 const visual = document.createElement("span");
1357 visual.className = `${TILE_CLASS}__visual`;
1358 if (thumbnail) {
1359 const img = document.createElement("img");
1360 img.src = thumbnail;
1361 img.alt = "";
1362 img.loading = "lazy";
1363 img.decoding = "async";
1364 img.className = `${TILE_CLASS}__preview`;
1365 img.draggable = false;
1366 visual.appendChild(img);
1367 } else if (icon) {
1368 const iconNode = renderIcon(icon, {
1369 title: label,
1370 className: `${TILE_CLASS}__icon`
1371 });
1372 visual.appendChild(iconNode);
1373 }
1374 this.appendChild(visual);
1375 const labelNode = document.createElement("span");
1376 labelNode.className = `${TILE_CLASS}__label`;
1377 labelNode.textContent = label;
1378 this.appendChild(labelNode);
1379 if (accessGated) {
1380 const lock = document.createElement("span");
1381 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
1382 lock.setAttribute("aria-hidden", "true");
1383 this.appendChild(lock);
1384 }
1385 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
1386 const ribbon = document.createElement("wpd-ribbon");
1387 ribbon.setAttribute("placement", "top-end");
1388 ribbon.setAttribute("tone", ribbonToneFor(status));
1389 ribbon.textContent = STATUS_LABEL[status];
1390 this.appendChild(ribbon);
1391 }
1392 applyTileEntryStagger(this);
1393 doAction("desktop-mode.tile.rendered", { tile: this });
1394 this._wireDragOut();
1395 }
1396 _wireDragOut() {
1397 if (this._pointerdownHandler) {
1398 this.removeEventListener(
1399 "pointerdown",
1400 this._pointerdownHandler
1401 );
1402 this._pointerdownHandler = null;
1403 }
1404 const dragKind = this.getAttribute("drag-kind");
1405 if (!dragKind) {
1406 return;
1407 }
1408 const handler = (e) => {
1409 if (e.button !== 0) {
1410 return;
1411 }
1412 const dragManager = getDragManager$1();
1413 if (!dragManager) {
1414 return;
1415 }
1416 const ref = this.getAttribute("ref") ?? "";
1417 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
1418 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
1419 const rect = this.getBoundingClientRect();
1420 dragManager.start({
1421 payload: {
1422 type: "shortcut",
1423 source: this,
1424 data: {
1425 kind: dragKind,
1426 ref,
1427 title,
1428 icon
1429 },
1430 ghost: {
1431 offsetX: e.clientX - rect.left,
1432 offsetY: e.clientY - rect.top
1433 }
1434 },
1435 origin: e
1436 });
1437 };
1438 this._pointerdownHandler = handler;
1439 this.addEventListener("pointerdown", handler);
1440 }
1441 };
1442 _WpdTile.shadow = false;
1443 _WpdTile.props = REACTIVE_PROPS;
1444 _WpdTile.styles = [styles$3];
1445 _WpdTile.help = {
1446 title: "Tile",
1447 summary: "Canonical file/entity tile. Used across the wallpaper, folder windows, every My WordPress section, and plugin surfaces. Renders the standard `.desktop-mode-file-tile` chrome + optional status ribbon and wires the shared drag-out helper.",
1448 status: "experimental",
1449 since: "0.21.0",
1450 props: [
1451 { name: "type", type: "string" },
1452 { name: "ref", type: "string" },
1453 { name: "label", type: "string" },
1454 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
1455 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
1456 { name: "kind", type: "`entry` | `folder`" },
1457 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
1458 { name: "selected", type: "boolean" },
1459 { name: "missing", type: "boolean" },
1460 { name: "access-gated", type: "boolean" },
1461 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
1462 { name: "drag-title", type: "string" },
1463 { name: "drag-icon", type: "string" }
1464 ]
1465 };
1466 let WpdTile = _WpdTile;
1467 function ribbonToneFor(status) {
1468 switch (status) {
1469 case "draft":
1470 return "warning";
1471 case "pending":
1472 return "info";
1473 case "private":
1474 return "danger";
1475 case "future":
1476 return "primary";
1477 default:
1478 return "primary";
1479 }
1480 }
1481 defineComponent("wpd-tile", WpdTile);
1482 function buildTileFromSpec(spec) {
1483 const tile = document.createElement("wpd-tile");
1484 tile.setAttribute("type", spec.type);
1485 tile.setAttribute("ref", spec.ref);
1486 tile.setAttribute("label", spec.label);
1487 if (spec.icon) {
1488 tile.setAttribute("icon", spec.icon);
1489 }
1490 if (spec.thumbnail) {
1491 tile.setAttribute("thumbnail", spec.thumbnail);
1492 }
1493 if (spec.role) {
1494 tile.setAttribute("kind", spec.role);
1495 }
1496 if (spec.status) {
1497 tile.setAttribute("status", spec.status);
1498 }
1499 if (spec.missing) {
1500 tile.setAttribute("missing", "");
1501 }
1502 if (spec.accessGated) {
1503 tile.setAttribute("access-gated", "");
1504 }
1505 if (spec.dataset) {
1506 for (const [key, raw] of Object.entries(spec.dataset)) {
1507 if (raw === void 0 || raw === null) {
1508 continue;
1509 }
1510 tile.dataset[key] = String(raw);
1511 }
1512 }
1513 if (Array.isArray(spec.extraClasses)) {
1514 for (const c of spec.extraClasses) {
1515 if (c) {
1516 tile.classList.add(c);
1517 }
1518 }
1519 }
1520 const classFiltered = applyFilters(
1521 "desktop-mode.tile.class",
1522 tile.className,
1523 spec
1524 );
1525 if (classFiltered && classFiltered !== tile.className) {
1526 tile.className = classFiltered;
1527 }
1528 if (typeof spec.x === "number" && typeof spec.y === "number") {
1529 tile.style.position = "absolute";
1530 tile.style.left = `${spec.x}px`;
1531 tile.style.top = `${spec.y}px`;
1532 }
1533 return tile;
1534 }
1535 function attachTileDragOut(tile, payload, onClick) {
1536 tile.addEventListener("pointerdown", (e) => {
1537 if (e.button !== 0) {
1538 return;
1539 }
1540 const dragManager = getDragManager$1();
1541 if (!dragManager) {
1542 return;
1543 }
1544 const rect = tile.getBoundingClientRect();
1545 dragManager.start({
1546 payload: {
1547 type: "shortcut",
1548 source: tile,
1549 data: {
1550 kind: payload.kind,
1551 ref: payload.ref,
1552 title: payload.title,
1553 icon: payload.icon,
1554 entityId: payload.entityId,
1555 bridgePayload: payload.bridgePayload
1556 },
1557 ghost: {
1558 offsetX: e.clientX - rect.left,
1559 offsetY: e.clientY - rect.top
1560 }
1561 },
1562 origin: e,
1563 onClickOnly: onClick
1564 });
1565 });
1566 }
1567 function getDragManager() {
1568 const api = window.wp?.desktop?.dragManager;
1569 return api ?? null;
1570 }
1571 function stripTags(html2) {
1572 const div = document.createElement("div");
1573 div.innerHTML = html2;
1574 return (div.textContent ?? "").trim();
1575 }
1576 const renderers = /* @__PURE__ */ new Map();
1577 function registerEntityKind(kind, renderer) {
1578 if (typeof kind !== "string" || kind === "") {
1579 throw new TypeError(
1580 "[my-wordpress] registerEntityKind: kind must be a non-empty string."
1581 );
1582 }
1583 if (typeof renderer !== "function") {
1584 throw new TypeError(
1585 "[my-wordpress] registerEntityKind: renderer must be a function."
1586 );
1587 }
1588 renderers.set(kind, renderer);
1589 return () => {
1590 if (renderers.get(kind) === renderer) {
1591 renderers.delete(kind);
1592 }
1593 };
1594 }
1595 function getEntityRenderer(kind) {
1596 if (!kind) {
1597 return renderers.get("post");
1598 }
1599 return renderers.get(kind);
1600 }
1601 const DEBOUNCE_MS = 300;
1602 function renderListToolbar(options) {
1603 const host = document.createElement("div");
1604 host.className = "desktop-mode-my-wordpress__list-toolbar";
1605 const search = document.createElement("div");
1606 search.className = "desktop-mode-my-wordpress__list-toolbar-search";
1607 const input = document.createElement("input");
1608 input.type = "search";
1609 input.className = "desktop-mode-my-wordpress__list-toolbar-search-input";
1610 input.placeholder = options.placeholder ?? __("Search…", "desktop-mode");
1611 input.setAttribute(
1612 "aria-label",
1613 options.ariaLabel ?? options.placeholder ?? __("Search", "desktop-mode")
1614 );
1615 input.autocomplete = "off";
1616 input.spellcheck = false;
1617 if (options.initialValue) {
1618 input.value = options.initialValue;
1619 }
1620 search.appendChild(input);
1621 host.appendChild(search);
1622 let debounceId = null;
1623 let lastEmitted = options.initialValue ?? "";
1624 const emit = (raw) => {
1625 const normalized = raw.trim();
1626 if (normalized === lastEmitted) {
1627 return;
1628 }
1629 lastEmitted = normalized;
1630 options.onSearchChange(normalized);
1631 };
1632 const onInput = () => {
1633 if (debounceId !== null) {
1634 clearTimeout(debounceId);
1635 }
1636 debounceId = setTimeout(() => {
1637 debounceId = null;
1638 emit(input.value);
1639 }, DEBOUNCE_MS);
1640 };
1641 input.addEventListener("input", onInput);
1642 const onSearchEvent = () => {
1643 if (input.value === "") {
1644 if (debounceId !== null) {
1645 clearTimeout(debounceId);
1646 debounceId = null;
1647 }
1648 emit("");
1649 }
1650 };
1651 input.addEventListener("search", onSearchEvent);
1652 const onKeydown = (ev) => {
1653 if (ev.key === "Enter") {
1654 ev.preventDefault();
1655 if (debounceId !== null) {
1656 clearTimeout(debounceId);
1657 debounceId = null;
1658 }
1659 emit(input.value);
1660 }
1661 };
1662 input.addEventListener("keydown", onKeydown);
1663 return {
1664 host,
1665 getQuery: () => lastEmitted,
1666 destroy: () => {
1667 if (debounceId !== null) {
1668 clearTimeout(debounceId);
1669 debounceId = null;
1670 }
1671 input.removeEventListener("input", onInput);
1672 input.removeEventListener("search", onSearchEvent);
1673 input.removeEventListener("keydown", onKeydown);
1674 }
1675 };
1676 }
1677 const FILE_DROP_HOOKS = {
1678 /**
1679 * Action — fires after a successful upload. Payload:
1680 * `{ file: File, result: DropUploadResult, fields:
1681 * DropDialogFields, context: DropContext }`.
1682 *
1683 * The `file` field carries the same `File` reference that
1684 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
1685 * payload returned by the `BEFORE_UPLOAD` filter, in case a
1686 * plugin swapped the file). Subscribers tracking per-file
1687 * state — progress HUDs, sequence counters — should match on
1688 * this identity rather than the filename: two drops of
1689 * `photo.jpg` from different folders would otherwise route
1690 * each other's success event to the wrong row.
1691 *
1692 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
1693 * that destructured `{ result, fields, context }` keeps working.
1694 */
1695 AFTER_UPLOAD: "desktop-mode.drop.after-upload"
1696 };
1697 const dialogStyles = css`:host{display:none;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba( 0,0,0,0.45 );backdrop-filter:blur( 2px );z-index:10000}:host( [ open ] ){display:flex}.dialog{width:min( 420px,92vw );background:var( --wpd-confirm-dialog-bg,var( --desktop-mode-bg,#1d2327 ) );color:var( --wpd-confirm-dialog-fg,var( --desktop-mode-fg,#fff ) );border:1px solid rgba( 255,255,255,0.08 );border-radius:10px;box-shadow:0 20px 50px rgba( 0,0,0,0.6 );padding:20px 22px 18px;display:flex;flex-direction:column;gap:10px;position:relative}.close{position:absolute;top:8px;right:10px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;border-radius:6px;color:var( --wpd-confirm-dialog-fg-muted,rgba( 255,255,255,0.7 ) );cursor:pointer;font-size:22px;line-height:1;padding:0}.close:hover{background:rgba( 255,255,255,0.08 );color:inherit}.title{margin:0 0 4px;font-size:16px;font-weight:600}.message{margin:0;color:var( --wpd-confirm-dialog-fg-muted,rgba( 255,255,255,0.7 ) );line-height:1.45;white-space:pre-line}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:6px}.btn{border:0;border-radius:6px;padding:8px 14px;font-size:13px;cursor:pointer;font-weight:500}.btn--secondary{background:rgba( 255,255,255,0.08 );color:inherit}.btn--secondary:hover{background:rgba( 255,255,255,0.14 )}.btn--primary{background:var( --wp-admin-theme-color,#2271b1 );color:#fff}.btn--primary:hover{filter:brightness( 1.08 )}.btn--danger{background:#d63638;color:#fff}.btn--danger:hover{filter:brightness( 1.08 )}`;
1698 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
1699 constructor() {
1700 super(...arguments);
1701 this._onKey = (e) => {
1702 if (e.key === "Escape") {
1703 e.preventDefault();
1704 this._cancel();
1705 }
1706 if (e.key === "Enter" && !e.isComposing) {
1707 e.preventDefault();
1708 this._confirm();
1709 }
1710 };
1711 this._onBackdrop = (e) => {
1712 const path = e.composedPath();
1713 const original = path.length > 0 ? path[0] : e.target;
1714 if (original === this) {
1715 this._cancel();
1716 }
1717 };
1718 this._confirm = () => {
1719 this.emit("wpd-confirm", { confirmed: true });
1720 this.removeAttribute("open");
1721 };
1722 this._cancel = () => {
1723 this.emit("wpd-cancel", { confirmed: false });
1724 this.removeAttribute("open");
1725 };
1726 }
1727 connectedCallback() {
1728 super.connectedCallback();
1729 this.setAttribute("role", "dialog");
1730 this.setAttribute("aria-modal", "true");
1731 this.addEventListener("keydown", this._onKey);
1732 this.addEventListener("click", this._onBackdrop);
1733 }
1734 disconnectedCallback() {
1735 this.removeEventListener("keydown", this._onKey);
1736 this.removeEventListener("click", this._onBackdrop);
1737 }
1738 render() {
1739 const title = this.title ?? "";
1740 const message = this.message ?? "";
1741 const confirmLabel = this["confirm-label"] || "Confirm";
1742 const cancelLabel = this["cancel-label"] || "Cancel";
1743 const isDanger = this.hasAttribute("danger");
1744 const hideCancel = this.hasAttribute("hide-cancel");
1745 const isDismissable = this.hasAttribute("dismissable");
1746 return html`
1747 <div class="dialog" tabindex="-1">
1748 ${isDismissable ? html`<button
1749 type="button"
1750 class="close"
1751 aria-label="Close"
1752 @click=${() => this._cancel()}
1753 >&times;</button>` : html``}
1754 ${title ? html`<h2 class="title">${title}</h2>` : html``}
1755 ${message ? html`<p class="message">${message}</p>` : html``}
1756 <div class="actions">
1757 ${hideCancel ? html`` : html`<button
1758 type="button"
1759 class="btn btn--secondary"
1760 @click=${() => this._cancel()}
1761 >
1762 ${cancelLabel}
1763 </button>`}
1764 <button
1765 type="button"
1766 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
1767 @click=${() => this._confirm()}
1768 >
1769 ${confirmLabel}
1770 </button>
1771 </div>
1772 </div>
1773 `;
1774 }
1775 };
1776 _WpdConfirmDialog.props = [
1777 "open",
1778 "title",
1779 "message",
1780 "confirm-label",
1781 "cancel-label",
1782 "danger",
1783 "hide-cancel",
1784 "dismissable"
1785 ];
1786 _WpdConfirmDialog.styles = [dialogStyles];
1787 _WpdConfirmDialog.help = {
1788 title: "Confirm dialog",
1789 summary: "Modal Yes/No replacement for window.confirm(). Two consumption paths: declarative element with `open` + `wpd-confirm` event, or the imperative Promise-returning `wpdConfirm()` helper.",
1790 status: "experimental",
1791 since: "0.9.0",
1792 props: [
1793 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
1794 { name: "title", type: "string", description: "Heading shown at the top." },
1795 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
1796 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
1797 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
1798 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
1799 { name: "hide-cancel", type: "boolean attribute", description: "Hides the cancel button entirely. Useful when there is no alternative action — pair with `dismissable` so the user still has an explicit way to close." },
1800 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
1801 ],
1802 events: [
1803 {
1804 name: "wpd-confirm",
1805 description: "Fires on confirm. Detail: `{ confirmed: true }`."
1806 },
1807 {
1808 name: "wpd-cancel",
1809 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
1810 }
1811 ]
1812 };
1813 let WpdConfirmDialog = _WpdConfirmDialog;
1814 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
1815 function wpdConfirm(options) {
1816 return new Promise((resolve) => {
1817 const dialog = document.createElement("wpd-confirm-dialog");
1818 dialog.setAttribute("open", "");
1819 if (options.title) {
1820 dialog.setAttribute("title", options.title);
1821 }
1822 dialog.setAttribute("message", options.message);
1823 if (options.confirmLabel) {
1824 dialog.setAttribute("confirm-label", options.confirmLabel);
1825 }
1826 if (options.cancelLabel) {
1827 dialog.setAttribute("cancel-label", options.cancelLabel);
1828 }
1829 {
1830 dialog.setAttribute("danger", "");
1831 }
1832 if (options.hideCancel) {
1833 dialog.setAttribute("hide-cancel", "");
1834 }
1835 if (options.dismissable) {
1836 dialog.setAttribute("dismissable", "");
1837 }
1838 const cleanup = (ok) => {
1839 dialog.remove();
1840 resolve(ok);
1841 };
1842 dialog.addEventListener("wpd-confirm", () => cleanup(true));
1843 dialog.addEventListener("wpd-cancel", () => cleanup(false));
1844 document.body.appendChild(dialog);
1845 const inner = dialog.shadowRoot?.querySelector(".dialog");
1846 (inner ?? dialog).focus?.();
1847 });
1848 }
1849 const HOOK_PREFIX = "desktop-mode.activity.";
1850 function hookName(channel) {
1851 return `${HOOK_PREFIX}${String(channel)}`;
1852 }
1853 let subscribeSeq = 0;
1854 const activity = {
1855 publish(channel, payload) {
1856 doAction(hookName(channel), payload);
1857 },
1858 subscribe(channel, cb) {
1859 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
1860 const hook = hookName(channel);
1861 addAction(
1862 hook,
1863 ns,
1864 (payload) => cb(payload)
1865 );
1866 let removed = false;
1867 return () => {
1868 if (removed) {
1869 return;
1870 }
1871 removed = true;
1872 removeAction(hook, ns);
1873 };
1874 },
1875 filter(channel, value, ...args) {
1876 return applyFilters(hookName(channel), value, ...args);
1877 }
1878 };
1879 const DEFAULT_DURATION_MS = 4e3;
1880 const FADE_OUT_MS = 200;
1881 function showToast$1(options) {
1882 const intent = activity.filter(
1883 "desktop-mode/toast-requested",
1884 { ...options }
1885 );
1886 if (!intent || intent.cancel === true) {
1887 return () => void 0;
1888 }
1889 let dismissRequested = false;
1890 let realDismiss = null;
1891 openWithShellOverlays(
1892 () => !dismissRequested,
1893 () => {
1894 realDismiss = renderToast(intent);
1895 }
1896 );
1897 return () => {
1898 dismissRequested = true;
1899 if (realDismiss) {
1900 realDismiss();
1901 }
1902 };
1903 }
1904 function renderToast(intent) {
1905 const container = ensureContainer();
1906 const toast = document.createElement("wpd-toast");
1907 toast.textContent = intent.message;
1908 if (intent.action) {
1909 toast.setAttribute("action", intent.action.label);
1910 toast.addEventListener("wpd-toast-action", () => {
1911 intent.action?.onClick();
1912 dismiss();
1913 });
1914 }
1915 container.appendChild(toast);
1916 let dismissed = false;
1917 let dismissTimer = null;
1918 const dismiss = () => {
1919 if (dismissed) {
1920 return;
1921 }
1922 dismissed = true;
1923 if (dismissTimer !== null) {
1924 window.clearTimeout(dismissTimer);
1925 dismissTimer = null;
1926 }
1927 toast.setAttribute("state", "out");
1928 window.setTimeout(() => {
1929 toast.remove();
1930 }, FADE_OUT_MS);
1931 };
1932 requestAnimationFrame(() => {
1933 toast.setAttribute("state", "in");
1934 });
1935 dismissTimer = window.setTimeout(
1936 dismiss,
1937 intent.duration ?? DEFAULT_DURATION_MS
1938 );
1939 activity.publish("desktop-mode/toast-shown", { ...intent });
1940 return dismiss;
1941 }
1942 function ensureContainer() {
1943 const existing = document.querySelector(
1944 "wpd-toast-container"
1945 );
1946 if (existing) {
1947 return existing;
1948 }
1949 const el = document.createElement("wpd-toast-container");
1950 document.body.appendChild(el);
1951 return el;
1952 }
1953 const FALLBACK_BASE = "http://localhost/";
1954 function joinRestUrl(restRoot, path) {
1955 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
1956 const url = new URL(restRoot, base);
1957 const trimmed = path.replace(/^\/+/, "");
1958 const queryAt = trimmed.indexOf("?");
1959 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
1960 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
1961 if (url.searchParams.has("rest_route")) {
1962 const existing = url.searchParams.get("rest_route") ?? "/";
1963 const prefix = existing.endsWith("/") ? existing : existing + "/";
1964 url.searchParams.set("rest_route", prefix + route);
1965 } else {
1966 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
1967 url.pathname = pathname + route;
1968 }
1969 if (extraQuery) {
1970 const extras = new URLSearchParams(extraQuery);
1971 extras.forEach((value, key) => {
1972 url.searchParams.append(key, value);
1973 });
1974 }
1975 return url.toString();
1976 }
1977 const NONCE_HEADER = "X-WP-Nonce";
1978 function injectRestNonce(input, init) {
1979 const nonce = readRestNonce();
1980 if (!nonce) {
1981 return init;
1982 }
1983 const url = resolveUrl(input);
1984 if (!url || !isSameOriginRestUrl(url)) {
1985 return init;
1986 }
1987 const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
1988 const headers = new Headers(baseHeaders ?? {});
1989 if (headers.has(NONCE_HEADER)) {
1990 return init;
1991 }
1992 headers.set(NONCE_HEADER, nonce);
1993 return { ...init ?? {}, headers };
1994 }
1995 function readRestNonce() {
1996 if (typeof window === "undefined") {
1997 return void 0;
1998 }
1999 const cfg = window.desktopModeConfig;
2000 const value = cfg?.restNonce;
2001 return typeof value === "string" && value.length > 0 ? value : void 0;
2002 }
2003 function resolveUrl(input) {
2004 try {
2005 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
2006 if (typeof input === "string") {
2007 return new URL(input, base);
2008 }
2009 if (input instanceof URL) {
2010 return input;
2011 }
2012 if (typeof Request !== "undefined" && input instanceof Request) {
2013 return new URL(input.url, base);
2014 }
2015 return null;
2016 } catch {
2017 return null;
2018 }
2019 }
2020 function isSameOriginRestUrl(url) {
2021 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
2022 return false;
2023 }
2024 if (url.pathname.includes("/wp-json/")) {
2025 return true;
2026 }
2027 if (url.searchParams.has("rest_route")) {
2028 return true;
2029 }
2030 return false;
2031 }
2032 function trackedFetch(input, init, opts = {}) {
2033 const fn = window.wp?.desktop?.fetch;
2034 if (typeof fn === "function") {
2035 return fn(input, init, opts);
2036 }
2037 const finalInit = injectRestNonce(input, init);
2038 return fetch(input, finalInit);
2039 }
2040 const WINDOW_ID$2 = "desktop-mode-my-wordpress";
2041 function getConfig() {
2042 const store = window.desktopModeWindowConfig;
2043 const cfg = store ? store[WINDOW_ID$2] : void 0;
2044 if (!cfg) {
2045 throw new Error(
2046 "[desktop-mode-my-wordpress] config blob missing — was the window opened without registration?"
2047 );
2048 }
2049 return cfg;
2050 }
2051 function getEntity(id) {
2052 return getConfig().entities.find((e) => e.id === id);
2053 }
2054 function buildUrl$1(path) {
2055 return joinRestUrl(getConfig().restRoot, path);
2056 }
2057 async function shellFetch$1(input, init) {
2058 return trackedFetch(input, init, {
2059 windowId: WINDOW_ID$2,
2060 source: "desktop-mode/my-wordpress"
2061 });
2062 }
2063 async function fetchEntityList(entity, params) {
2064 const cfg = getConfig();
2065 const url = new URL(buildUrl$1(entity.restPath));
2066 url.searchParams.set("page", String(params.page));
2067 url.searchParams.set("per_page", String(params.perPage));
2068 url.searchParams.set(
2069 "_fields",
2070 "id,title,excerpt,date,status,featured_media,link,desktop_mode_lock,_links,_embedded"
2071 );
2072 url.searchParams.set("_embed", "wp:featuredmedia");
2073 url.searchParams.set("status", "publish,future,draft,pending,private");
2074 if (params.search) {
2075 url.searchParams.set("search", params.search);
2076 }
2077 const response = await shellFetch$1(url.toString(), {
2078 method: "GET",
2079 credentials: "same-origin",
2080 headers: {
2081 "X-WP-Nonce": cfg.restNonce,
2082 Accept: "application/json"
2083 },
2084 signal: params.signal
2085 });
2086 if (!response.ok) {
2087 throw new Error(
2088 await readErrorMessage$1(response, "Failed to load list")
2089 );
2090 }
2091 const items = await response.json();
2092 const total = Number(response.headers.get("X-WP-Total") ?? items.length);
2093 const totalPages = Number(
2094 response.headers.get("X-WP-TotalPages") ?? 1
2095 );
2096 return { items, total, totalPages };
2097 }
2098 async function fetchEntityDetail(entity, id) {
2099 const cfg = getConfig();
2100 const url = new URL(buildUrl$1(`${entity.restPath}/${id}`));
2101 url.searchParams.set(
2102 "_fields",
2103 "id,title,content,excerpt,date,modified,status,link,author,featured_media,categories,tags,comment_status,desktop_mode_contributors,desktop_mode_attached_media,_links,_embedded"
2104 );
2105 url.searchParams.set("_embed", "author,wp:term,wp:featuredmedia,replies");
2106 const response = await shellFetch$1(url.toString(), {
2107 method: "GET",
2108 credentials: "same-origin",
2109 headers: {
2110 "X-WP-Nonce": cfg.restNonce,
2111 Accept: "application/json"
2112 }
2113 });
2114 if (!response.ok) {
2115 throw new Error(
2116 await readErrorMessage$1(response, "Failed to load entry")
2117 );
2118 }
2119 return await response.json();
2120 }
2121 async function trashEntity(entity, id) {
2122 const cfg = getConfig();
2123 const url = buildUrl$1(`${entity.restPath}/${id}`);
2124 const response = await shellFetch$1(url, {
2125 method: "DELETE",
2126 credentials: "same-origin",
2127 headers: {
2128 "X-WP-Nonce": cfg.restNonce,
2129 Accept: "application/json"
2130 }
2131 });
2132 if (!response.ok) {
2133 throw new Error(
2134 await readErrorMessage$1(response, "Failed to move to trash")
2135 );
2136 }
2137 }
2138 async function readErrorMessage$1(response, fallback) {
2139 let message = `${response.status} ${response.statusText || fallback}`;
2140 try {
2141 const json = await response.json();
2142 if (json && typeof json.message === "string") {
2143 message = json.message;
2144 }
2145 } catch {
2146 }
2147 return message;
2148 }
2149 async function fetchEntityTotal(entity) {
2150 const cfg = getConfig();
2151 const buildRequestUrl = (withWho) => {
2152 const url = new URL(buildUrl$1(entity.restPath));
2153 url.searchParams.set("page", "1");
2154 url.searchParams.set("per_page", "1");
2155 url.searchParams.set("_fields", "id");
2156 if (entity.kind === "user") {
2157 if (withWho) {
2158 url.searchParams.set("who", "authors");
2159 }
2160 } else if (entity.kind === "media") {
2161 url.searchParams.set("status", "inherit");
2162 } else {
2163 url.searchParams.set("status", "publish,future,draft,pending,private");
2164 }
2165 return url.toString();
2166 };
2167 const send = (target) => shellFetch$1(target, {
2168 method: "GET",
2169 credentials: "same-origin",
2170 headers: {
2171 "X-WP-Nonce": cfg.restNonce,
2172 Accept: "application/json"
2173 }
2174 });
2175 let response = await send(buildRequestUrl(false));
2176 if (response.status === 403 && entity.kind === "user") {
2177 response = await send(buildRequestUrl(true));
2178 }
2179 if (!response.ok) {
2180 throw new Error(await readErrorMessage$1(response, "Failed to count"));
2181 }
2182 await response.json().catch(() => null);
2183 const raw = response.headers.get("X-WP-Total");
2184 const n = raw ? Number(raw) : NaN;
2185 return Number.isFinite(n) ? n : 0;
2186 }
2187 async function fetchUserList(entity, params) {
2188 const cfg = getConfig();
2189 const buildRequestUrl = (mode) => {
2190 const url = new URL(buildUrl$1(entity.restPath));
2191 url.searchParams.set("page", String(params.page));
2192 url.searchParams.set("per_page", String(params.perPage));
2193 url.searchParams.set(
2194 "_fields",
2195 "id,name,slug,description,link,avatar_urls,desktop_mode_summary"
2196 );
2197 url.searchParams.set("orderby", "name");
2198 url.searchParams.set("order", "asc");
2199 if (mode === "edit") {
2200 url.searchParams.set("context", "edit");
2201 } else {
2202 url.searchParams.set("who", "authors");
2203 }
2204 if (params.search) {
2205 url.searchParams.set("search", params.search);
2206 }
2207 return url.toString();
2208 };
2209 const send = (target) => shellFetch$1(target, {
2210 method: "GET",
2211 credentials: "same-origin",
2212 headers: {
2213 "X-WP-Nonce": cfg.restNonce,
2214 Accept: "application/json"
2215 },
2216 signal: params.signal
2217 });
2218 let response = await send(buildRequestUrl("edit"));
2219 if (response.status === 403) {
2220 response = await send(buildRequestUrl("authors"));
2221 }
2222 if (!response.ok) {
2223 throw new Error(await readErrorMessage$1(response, "Failed to load users"));
2224 }
2225 const items = await response.json();
2226 const total = Number(response.headers.get("X-WP-Total") ?? items.length);
2227 const totalPages = Number(
2228 response.headers.get("X-WP-TotalPages") ?? 1
2229 );
2230 return { items, total, totalPages };
2231 }
2232 function fetchUserFootprint(userId) {
2233 return getJson(
2234 buildUrl$1(`desktop-mode/v1/user-footprint/${userId}`)
2235 );
2236 }
2237 function buildEditUserUrl(id) {
2238 const cfg = getConfig();
2239 const base = cfg.editUserUrlBase || cfg.editPostUrlBase;
2240 const sep = base.includes("?") ? "&" : "?";
2241 return `${base}${sep}user_id=${encodeURIComponent(String(id))}`;
2242 }
2243 function buildEditUrl(id) {
2244 const cfg = getConfig();
2245 const base = cfg.editPostUrlBase;
2246 const sep = base.includes("?") ? "&" : "?";
2247 return `${base}${sep}post=${encodeURIComponent(String(id))}&action=edit`;
2248 }
2249 async function getJson(url) {
2250 const cfg = getConfig();
2251 const response = await shellFetch$1(url, {
2252 method: "GET",
2253 credentials: "same-origin",
2254 headers: {
2255 "X-WP-Nonce": cfg.restNonce,
2256 Accept: "application/json"
2257 }
2258 });
2259 if (!response.ok) {
2260 throw new Error(await readErrorMessage$1(response, "Failed to load"));
2261 }
2262 return await response.json();
2263 }
2264 function fetchUserStats(id) {
2265 return getJson(buildUrl$1(`desktop-mode/v1/user-stats/${id}`));
2266 }
2267 function fetchTermStats(taxonomy, id) {
2268 const slug = taxonomy.replace(/[^a-zA-Z0-9_-]/g, "");
2269 return getJson(
2270 buildUrl$1(`desktop-mode/v1/term-stats/${slug}/${id}`)
2271 );
2272 }
2273 function fetchCommentStats(id) {
2274 return getJson(
2275 buildUrl$1(`desktop-mode/v1/comment-stats/${id}`)
2276 );
2277 }
2278 function fetchUser(id) {
2279 return getJson(
2280 buildUrl$1(`wp/v2/users/${id}?context=edit&_fields=id,name,slug,description,avatar_urls,link`)
2281 );
2282 }
2283 function fetchComments(postId) {
2284 return getJson(
2285 buildUrl$1(
2286 `wp/v2/comments?post=${postId}&per_page=100&_fields=id,post,author,author_name,author_avatar_urls,date,content,status,parent`
2287 )
2288 );
2289 }
2290 function fetchTerms(taxonomy, ids) {
2291 if (ids.length === 0) {
2292 return Promise.resolve([]);
2293 }
2294 return getJson(
2295 buildUrl$1(
2296 `wp/v2/${taxonomy}?include=${ids.join(",")}&per_page=100&_fields=id,name,slug,taxonomy,description,count,link`
2297 )
2298 );
2299 }
2300 function fetchAttachedMedia(postId) {
2301 return getJson(
2302 buildUrl$1(
2303 `wp/v2/media?parent=${postId}&per_page=100&_fields=id,title,source_url,mime_type,alt_text,date,media_details`
2304 )
2305 );
2306 }
2307 function fetchMediaByIds(ids) {
2308 const unique = Array.from(new Set(ids.filter((id) => id > 0)));
2309 if (unique.length === 0) {
2310 return Promise.resolve([]);
2311 }
2312 return getJson(
2313 buildUrl$1(
2314 `wp/v2/media?include=${unique.join(",")}&per_page=${unique.length}&_fields=id,title,source_url,mime_type,alt_text,date,media_details`
2315 )
2316 );
2317 }
2318 function fetchRevisions(entity, postId) {
2319 return getJson(
2320 buildUrl$1(
2321 `${entity.restPath}/${postId}/revisions?_fields=id,date,modified,author,title`
2322 )
2323 );
2324 }
2325 function fetchRevision(entity, postId, revisionId) {
2326 return getJson(
2327 buildUrl$1(
2328 `${entity.restPath}/${postId}/revisions/${revisionId}?_fields=id,date,modified,author,title,content,excerpt`
2329 )
2330 );
2331 }
2332 const WINDOW_ID$1 = "desktop-mode-my-wordpress";
2333 function shellFetch(input, init) {
2334 return trackedFetch(input, init, {
2335 windowId: WINDOW_ID$1,
2336 source: "desktop-mode/my-wordpress"
2337 });
2338 }
2339 function buildUrl(path) {
2340 return joinRestUrl(getConfig().restRoot, path);
2341 }
2342 async function readErrorMessage(response, fallback) {
2343 let message = `${response.status} ${response.statusText || fallback}`;
2344 try {
2345 const json = await response.json();
2346 if (json && typeof json.message === "string") {
2347 message = json.message;
2348 }
2349 } catch {
2350 }
2351 return message;
2352 }
2353 async function fetchMediaPage(entity, params) {
2354 const cfg = getConfig();
2355 const url = new URL(buildUrl(entity.restPath));
2356 url.searchParams.set("page", String(params.page));
2357 url.searchParams.set("per_page", String(params.perPage));
2358 url.searchParams.set(
2359 "_fields",
2360 "id,title,date,mime_type,source_url,alt_text,caption,description,author,media_details,_embedded"
2361 );
2362 url.searchParams.set("_embed", "author");
2363 url.searchParams.set("orderby", "date");
2364 url.searchParams.set("order", "desc");
2365 url.searchParams.set("status", "inherit");
2366 if (params.search) {
2367 url.searchParams.set("search", params.search);
2368 }
2369 const response = await shellFetch(url.toString(), {
2370 method: "GET",
2371 credentials: "same-origin",
2372 headers: {
2373 "X-WP-Nonce": cfg.restNonce,
2374 Accept: "application/json"
2375 },
2376 signal: params.signal
2377 });
2378 if (!response.ok) {
2379 throw new Error(
2380 await readErrorMessage(response, "Failed to load media")
2381 );
2382 }
2383 const items = await response.json();
2384 const total = Number(response.headers.get("X-WP-Total") ?? items.length);
2385 const totalPages = Number(
2386 response.headers.get("X-WP-TotalPages") ?? 1
2387 );
2388 return { items, total, totalPages };
2389 }
2390 async function fetchMediaItem(mediaId) {
2391 const cfg = getConfig();
2392 const url = new URL(buildUrl(`wp/v2/media/${mediaId}`));
2393 url.searchParams.set(
2394 "_fields",
2395 "id,title,date,mime_type,source_url,alt_text,caption,description,author,media_details,_embedded"
2396 );
2397 url.searchParams.set("_embed", "author");
2398 const response = await shellFetch(url.toString(), {
2399 method: "GET",
2400 credentials: "same-origin",
2401 headers: {
2402 "X-WP-Nonce": cfg.restNonce,
2403 Accept: "application/json"
2404 }
2405 });
2406 if (!response.ok) {
2407 throw new Error(
2408 await readErrorMessage(response, "Failed to load media item")
2409 );
2410 }
2411 return await response.json();
2412 }
2413 async function deleteMediaItem(mediaId) {
2414 const cfg = getConfig();
2415 const url = new URL(buildUrl(`wp/v2/media/${mediaId}`));
2416 url.searchParams.set("force", "true");
2417 const response = await shellFetch(url.toString(), {
2418 method: "DELETE",
2419 credentials: "same-origin",
2420 headers: {
2421 "X-WP-Nonce": cfg.restNonce,
2422 Accept: "application/json"
2423 }
2424 });
2425 if (!response.ok) {
2426 throw new Error(
2427 await readErrorMessage(response, "Failed to delete media item")
2428 );
2429 }
2430 }
2431 async function fetchMediaUsage(mediaId) {
2432 const cfg = getConfig();
2433 const response = await shellFetch(
2434 buildUrl(`desktop-mode/v1/media-usage/${mediaId}`),
2435 {
2436 method: "GET",
2437 credentials: "same-origin",
2438 headers: {
2439 "X-WP-Nonce": cfg.restNonce,
2440 Accept: "application/json"
2441 }
2442 }
2443 );
2444 if (!response.ok) {
2445 throw new Error(
2446 await readErrorMessage(response, "Failed to load media usage")
2447 );
2448 }
2449 return await response.json();
2450 }
2451 const MIME_DASHICON_FALLBACK = "dashicons-media-default";
2452 const MIME_DASHICON_MAP = [
2453 { test: /^image\//, icon: "dashicons-format-image" },
2454 { test: /^video\//, icon: "dashicons-format-video" },
2455 { test: /^audio\//, icon: "dashicons-format-audio" },
2456 { test: /pdf$/, icon: "dashicons-media-document" },
2457 { test: /^application\/(zip|x-tar|x-rar|x-7z)/, icon: "dashicons-media-archive" },
2458 { test: /spreadsheet|excel/, icon: "dashicons-media-spreadsheet" },
2459 { test: /word|document/, icon: "dashicons-media-document" },
2460 { test: /^text\//, icon: "dashicons-media-text" }
2461 ];
2462 function dashiconForMime(mime) {
2463 for (const entry of MIME_DASHICON_MAP) {
2464 if (entry.test.test(mime)) {
2465 return entry.icon;
2466 }
2467 }
2468 return MIME_DASHICON_FALLBACK;
2469 }
2470 function mimeGroup(mime) {
2471 if (mime.startsWith("image/")) {
2472 return "image";
2473 }
2474 if (mime.startsWith("video/")) {
2475 return "video";
2476 }
2477 if (mime.startsWith("audio/")) {
2478 return "audio";
2479 }
2480 return "doc";
2481 }
2482 function formatBytes(bytes) {
2483 if (!bytes || !Number.isFinite(bytes)) {
2484 return "";
2485 }
2486 if (bytes < 1024) {
2487 return `${bytes} B`;
2488 }
2489 const units = ["KB", "MB", "GB", "TB"];
2490 let value = bytes / 1024;
2491 let unit = 0;
2492 while (value >= 1024 && unit < units.length - 1) {
2493 value /= 1024;
2494 unit += 1;
2495 }
2496 return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`;
2497 }
2498 function formatDate$1(iso) {
2499 if (!iso) {
2500 return "";
2501 }
2502 const d = new Date(iso);
2503 if (Number.isNaN(d.valueOf())) {
2504 return iso;
2505 }
2506 return d.toLocaleDateString(void 0, {
2507 year: "numeric",
2508 month: "short",
2509 day: "numeric"
2510 });
2511 }
2512 function buildMediaVisual(media) {
2513 const wrap = document.createElement("div");
2514 wrap.className = "desktop-mode-my-wordpress__media-visual";
2515 const group = mimeGroup(media.mime_type);
2516 if (group === "image") {
2517 const img = document.createElement("img");
2518 const sizes = media.media_details?.sizes;
2519 img.src = sizes?.large?.source_url ?? sizes?.medium_large?.source_url ?? sizes?.medium?.source_url ?? media.source_url;
2520 img.alt = media.alt_text ?? stripTags(media.title.rendered);
2521 img.loading = "lazy";
2522 img.decoding = "async";
2523 img.className = "desktop-mode-my-wordpress__media-image";
2524 wrap.appendChild(img);
2525 return wrap;
2526 }
2527 if (group === "video") {
2528 const video = document.createElement("video");
2529 video.controls = true;
2530 video.preload = "metadata";
2531 video.src = media.source_url;
2532 const sizes = media.media_details?.sizes;
2533 const poster = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? "";
2534 if (poster) {
2535 video.poster = poster;
2536 }
2537 video.className = "desktop-mode-my-wordpress__media-video";
2538 wrap.appendChild(video);
2539 return wrap;
2540 }
2541 if (group === "audio") {
2542 const stack = document.createElement("div");
2543 stack.className = "desktop-mode-my-wordpress__media-audio-stack";
2544 const icon2 = document.createElement("span");
2545 icon2.className = "desktop-mode-my-wordpress__media-fallback-icon dashicons " + dashiconForMime(media.mime_type);
2546 icon2.setAttribute("aria-hidden", "true");
2547 stack.appendChild(icon2);
2548 const audio = document.createElement("audio");
2549 audio.controls = true;
2550 audio.preload = "metadata";
2551 audio.src = media.source_url;
2552 audio.className = "desktop-mode-my-wordpress__media-audio";
2553 stack.appendChild(audio);
2554 wrap.appendChild(stack);
2555 return wrap;
2556 }
2557 const icon = document.createElement("span");
2558 icon.className = "desktop-mode-my-wordpress__media-fallback-icon dashicons " + dashiconForMime(media.mime_type);
2559 icon.setAttribute("aria-hidden", "true");
2560 wrap.appendChild(icon);
2561 const link = document.createElement("a");
2562 link.href = media.source_url;
2563 link.target = "_blank";
2564 link.rel = "noopener noreferrer";
2565 link.className = "desktop-mode-my-wordpress__media-doc-link";
2566 link.textContent = __("Open file", "desktop-mode");
2567 wrap.appendChild(link);
2568 return wrap;
2569 }
2570 function buildMetaRow(label, value) {
2571 if (typeof value === "string" && value.trim() === "") {
2572 return null;
2573 }
2574 const dt = document.createElement("dt");
2575 dt.className = "desktop-mode-my-wordpress__media-meta-term";
2576 dt.textContent = label;
2577 const dd = document.createElement("dd");
2578 dd.className = "desktop-mode-my-wordpress__media-meta-value";
2579 if (typeof value === "string") {
2580 dd.textContent = value;
2581 } else {
2582 dd.appendChild(value);
2583 }
2584 return [dt, dd];
2585 }
2586 function buildMetadataGrid(media) {
2587 const grid = document.createElement("dl");
2588 grid.className = "desktop-mode-my-wordpress__media-meta";
2589 const filename = media.media_details?.file ? media.media_details.file.split("/").pop() ?? "" : media.source_url.split("/").pop() ?? "";
2590 const dims = media.media_details?.width && media.media_details?.height ? `${media.media_details.width} × ${media.media_details.height}` : "";
2591 const filesize = formatBytes(media.media_details?.filesize);
2592 const uploaded = formatDate$1(media.date);
2593 const uploader = media._embedded?.author?.[0]?.name ?? "";
2594 const alt = (media.alt_text ?? "").trim();
2595 const caption = stripTags(media.caption?.rendered ?? "");
2596 const description = stripTags(media.description?.rendered ?? "");
2597 const rows = [
2598 buildMetaRow(__("Filename", "desktop-mode"), filename),
2599 buildMetaRow(__("Type", "desktop-mode"), media.mime_type),
2600 buildMetaRow(__("Dimensions", "desktop-mode"), dims),
2601 buildMetaRow(__("File size", "desktop-mode"), filesize),
2602 buildMetaRow(__("Uploaded", "desktop-mode"), uploaded),
2603 buildMetaRow(__("Uploader", "desktop-mode"), uploader),
2604 buildMetaRow(__("Alt text", "desktop-mode"), alt),
2605 buildMetaRow(__("Caption", "desktop-mode"), caption),
2606 buildMetaRow(__("Description", "desktop-mode"), description)
2607 ];
2608 for (const pair of rows) {
2609 if (pair) {
2610 grid.append(...pair);
2611 }
2612 }
2613 return grid;
2614 }
2615 function fireSlot(host, slot, entityId, kind, item) {
2616 doAction(
2617 "desktop-mode.my-wordpress.preview-extras",
2618 {
2619 slot,
2620 container: host,
2621 entityId,
2622 kind,
2623 item
2624 }
2625 );
2626 }
2627 function resolvePreviewActions(descriptors, ctx) {
2628 const scoped = descriptors.filter((a) => {
2629 if (a.sections && a.sections.length > 0) {
2630 if (!a.sections.includes(ctx.entityId) && !a.sections.includes("*")) {
2631 return false;
2632 }
2633 }
2634 if (a.mime) {
2635 if (!ctx.mime) {
2636 return false;
2637 }
2638 try {
2639 const re = new RegExp(a.mime);
2640 if (!re.test(ctx.mime)) {
2641 return false;
2642 }
2643 } catch {
2644 return false;
2645 }
2646 }
2647 return true;
2648 });
2649 const merged = applyFilters("desktop-mode.my-wordpress.preview-actions", scoped, ctx);
2650 return Array.isArray(merged) ? merged : scoped;
2651 }
2652 function buildActionRow(actions, ctx) {
2653 const visible = actions.filter(
2654 (a) => typeof a.isVisible === "function" ? a.isVisible(ctx) : true
2655 );
2656 if (visible.length === 0) {
2657 return null;
2658 }
2659 const row = document.createElement("div");
2660 row.className = "desktop-mode-my-wordpress__media-actions";
2661 row.setAttribute("role", "toolbar");
2662 for (const action of visible) {
2663 const btn = document.createElement("wpd-button");
2664 btn.setAttribute("variant", "secondary");
2665 btn.dataset.actionId = action.id;
2666 if (action.icon) {
2667 btn.setAttribute("icon", action.icon);
2668 }
2669 btn.textContent = action.label;
2670 btn.addEventListener("click", () => {
2671 if (typeof action.onSelect === "function") {
2672 try {
2673 void action.onSelect(ctx);
2674 } catch {
2675 console.error(
2676 `[my-wordpress] preview action ${action.id} threw.`
2677 );
2678 }
2679 }
2680 });
2681 row.appendChild(btn);
2682 }
2683 return row;
2684 }
2685 function renderMediaPreview(host, media, opts) {
2686 host.replaceChildren();
2687 const pane = document.createElement("div");
2688 pane.className = "desktop-mode-my-wordpress__media-pane";
2689 const header = document.createElement("header");
2690 header.className = "desktop-mode-my-wordpress__media-header";
2691 const heading = document.createElement("h2");
2692 heading.className = "desktop-mode-my-wordpress__media-title";
2693 heading.textContent = stripTags(media.title.rendered) || __("(no title)", "desktop-mode");
2694 header.appendChild(heading);
2695 pane.appendChild(header);
2696 const item = media;
2697 const ctx = {
2698 entityId: opts.entityId,
2699 kind: "media",
2700 mime: media.mime_type,
2701 item
2702 };
2703 fireSlot(header, "header", opts.entityId, "media", item);
2704 pane.appendChild(buildMediaVisual(media));
2705 const meta = buildMetadataGrid(media);
2706 pane.appendChild(meta);
2707 fireSlot(meta, "meta", opts.entityId, "media", item);
2708 const resolved = resolvePreviewActions(opts.previewActions, ctx);
2709 const actionRow = buildActionRow(resolved, ctx);
2710 if (actionRow) {
2711 pane.appendChild(actionRow);
2712 }
2713 if (opts.onOpenDetail) {
2714 const footer = document.createElement("footer");
2715 footer.className = "desktop-mode-my-wordpress__article-footer";
2716 const drillBtn = document.createElement("wpd-button");
2717 drillBtn.setAttribute("variant", "primary");
2718 drillBtn.textContent = __("See where this is used", "desktop-mode");
2719 drillBtn.title = __(
2720 "Show the posts, pages, and custom-post-type entries that reference this file.",
2721 "desktop-mode"
2722 );
2723 drillBtn.addEventListener("click", () => opts.onOpenDetail?.());
2724 footer.appendChild(drillBtn);
2725 pane.appendChild(footer);
2726 fireSlot(footer, "footer", opts.entityId, "media", item);
2727 } else {
2728 const footer = document.createElement("div");
2729 footer.className = "desktop-mode-my-wordpress__media-footer";
2730 pane.appendChild(footer);
2731 fireSlot(footer, "footer", opts.entityId, "media", item);
2732 }
2733 host.appendChild(pane);
2734 }
2735 const lastQueryByMediaEntity = /* @__PURE__ */ new Map();
2736 function describeCount(ctx) {
2737 if (ctx.total === 0 && ctx.loaded === 0) {
2738 return __("No media yet.", "desktop-mode");
2739 }
2740 if (ctx.total > ctx.loaded && ctx.loaded > 0) {
2741 return sprintf(
2742 // translators: 1: visible item count, 2: total item count.
2743 __("%1$d of %2$d items", "desktop-mode"),
2744 ctx.loaded,
2745 ctx.total
2746 );
2747 }
2748 const n = Math.max(ctx.total, ctx.loaded);
2749 return sprintf(
2750 // translators: %d is a count of media items.
2751 _n("%d item", "%d items", n),
2752 n
2753 );
2754 }
2755 function paintStatus$2(ctx) {
2756 const segments = [
2757 {
2758 id: "count",
2759 label: describeCount(ctx),
2760 align: "start",
2761 sort: 10
2762 }
2763 ];
2764 if (ctx.totalPages > 1) {
2765 segments.push({
2766 id: "page",
2767 label: sprintf(
2768 // translators: 1: current page, 2: total pages.
2769 __("Page %1$d of %2$d", "desktop-mode"),
2770 Math.max(ctx.page, 1),
2771 ctx.totalPages
2772 ),
2773 align: "end",
2774 sort: 10
2775 });
2776 }
2777 const filtered = applyFilters(
2778 "desktop-mode.my-wordpress.status-bar",
2779 segments,
2780 { view: "list", entityId: ctx.entity.id }
2781 );
2782 renderStatusBarSegments(
2783 ctx.statusBar,
2784 Array.isArray(filtered) ? filtered : segments
2785 );
2786 }
2787 function buildMediaTile(ctx, media) {
2788 const titleText = stripTags(media.title.rendered) || __("(no title)", "desktop-mode");
2789 const sizes = media.media_details?.sizes;
2790 const thumbUrl = media.mime_type.startsWith("image/") ? sizes?.thumbnail?.source_url ?? sizes?.medium?.source_url ?? media.source_url : "";
2791 const tile = buildTileFromSpec({
2792 type: "attachment",
2793 ref: String(media.id),
2794 label: titleText,
2795 thumbnail: thumbUrl || void 0,
2796 icon: thumbUrl ? void 0 : dashiconForMime(media.mime_type),
2797 role: "entry",
2798 dataset: { mediaId: media.id, mime: media.mime_type },
2799 extraClasses: [
2800 "desktop-mode-my-wordpress__media-tile",
2801 "desktop-mode-my-wordpress__tile",
2802 "desktop-mode-my-wordpress__tile--media"
2803 ]
2804 });
2805 attachTileDragOut(tile, {
2806 kind: "attachment",
2807 ref: String(media.id),
2808 title: titleText,
2809 icon: dashiconForMime(media.mime_type),
2810 // Cross-frame bridge payload — lets the Gutenberg drop-
2811 // receiver build a `core/image` / `core/video` / `core/audio`
2812 // / `core/file` block when this tile is dropped on an open
2813 // editor iframe. The full-size source URL is the right block
2814 // attribute regardless of mime; the receiver picks the
2815 // concrete block from the MIME prefix.
2816 bridgePayload: {
2817 kind: "attachment",
2818 id: media.id,
2819 url: media.source_url,
2820 title: titleText,
2821 alt: stripTags(media.alt_text ?? ""),
2822 mime: media.mime_type,
2823 thumbnailUrl: thumbUrl || void 0,
2824 sizes: media.media_details?.sizes
2825 }
2826 });
2827 tile.addEventListener("click", () => selectTile$1(ctx, tile, media));
2828 tile.addEventListener("dblclick", (e) => {
2829 e.preventDefault();
2830 ctx.host.navigate({
2831 kind: "media-detail",
2832 entityId: ctx.entity.id,
2833 mediaId: media.id,
2834 mediaTitle: titleText
2835 });
2836 });
2837 tile.addEventListener("contextmenu", (e) => {
2838 e.preventDefault();
2839 openMediaTileMenu(ctx, tile, media, titleText, {
2840 x: e.clientX,
2841 y: e.clientY
2842 });
2843 });
2844 return tile;
2845 }
2846 function openMediaTileMenu(ctx, tile, media, titleText, pos) {
2847 closeAnyMediaTileMenu();
2848 selectTile$1(ctx, tile, media);
2849 const menu = document.createElement("wpd-context-menu");
2850 menu.setAttribute("open", "");
2851 menu.classList.add("desktop-mode-my-wordpress__menu");
2852 menu.style.left = `${pos.x}px`;
2853 menu.style.top = `${pos.y}px`;
2854 const base = [
2855 {
2856 id: "navigate-into",
2857 label: __("Navigate into", "desktop-mode"),
2858 icon: "dashicons-category"
2859 },
2860 {
2861 id: "open-source",
2862 label: __("Open file in new tab", "desktop-mode"),
2863 icon: "dashicons-external"
2864 },
2865 {
2866 id: "delete",
2867 label: __("Delete permanently", "desktop-mode"),
2868 icon: "dashicons-trash",
2869 danger: true
2870 }
2871 ];
2872 const filterCtx = {
2873 entityId: ctx.entity.id,
2874 kind: "attachment",
2875 item: media
2876 };
2877 const options = applyFilters(
2878 "desktop-mode.my-wordpress.tile-context-menu",
2879 base,
2880 filterCtx
2881 );
2882 const finalOptions = Array.isArray(options) ? options : base;
2883 for (const o of finalOptions) {
2884 const opt = document.createElement("wpd-context-menu-option");
2885 opt.dataset.menuItemId = o.id;
2886 opt.setAttribute("value", o.id);
2887 opt.setAttribute("icon", o.icon);
2888 if (o.danger) {
2889 opt.setAttribute("danger", "");
2890 }
2891 opt.textContent = o.label;
2892 menu.appendChild(opt);
2893 }
2894 menu.addEventListener("wpd-context-menu-pick", (e) => {
2895 const detail = e.detail;
2896 closeAnyMediaTileMenu();
2897 if (detail.id === "navigate-into") {
2898 ctx.host.navigate({
2899 kind: "media-detail",
2900 entityId: ctx.entity.id,
2901 mediaId: media.id,
2902 mediaTitle: titleText
2903 });
2904 return;
2905 }
2906 if (detail.id === "open-source") {
2907 window.open(media.source_url, "_blank", "noopener,noreferrer");
2908 return;
2909 }
2910 if (detail.id === "delete") {
2911 void confirmDeleteMedia(ctx, tile, media, titleText);
2912 return;
2913 }
2914 const match = finalOptions.find((o) => o.id === detail.id);
2915 if (match && typeof match.onSelect === "function") {
2916 try {
2917 match.onSelect();
2918 } catch (err) {
2919 console.error(
2920 `[my-wordpress/media] tile-context-menu '${detail.id}' onSelect threw:`,
2921 err
2922 );
2923 }
2924 }
2925 });
2926 document.body.appendChild(menu);
2927 const rect = menu.getBoundingClientRect();
2928 if (rect.right > window.innerWidth) {
2929 menu.style.left = `${Math.max(
2930 0,
2931 window.innerWidth - rect.width - 8
2932 )}px`;
2933 }
2934 if (rect.bottom > window.innerHeight) {
2935 menu.style.top = `${Math.max(
2936 0,
2937 window.innerHeight - rect.height - 8
2938 )}px`;
2939 }
2940 queueMicrotask(() => {
2941 const onDocPointerDown = (ev) => {
2942 if (ev.target instanceof Node && menu.contains(ev.target)) {
2943 return;
2944 }
2945 closeAnyMediaTileMenu();
2946 };
2947 const onDocKey = (ev) => {
2948 if (ev.key === "Escape") {
2949 closeAnyMediaTileMenu();
2950 }
2951 };
2952 document.addEventListener("pointerdown", onDocPointerDown, true);
2953 document.addEventListener("keydown", onDocKey);
2954 menu.addEventListener("tile-menu-closed", () => {
2955 document.removeEventListener(
2956 "pointerdown",
2957 onDocPointerDown,
2958 true
2959 );
2960 document.removeEventListener("keydown", onDocKey);
2961 });
2962 });
2963 }
2964 function closeAnyMediaTileMenu() {
2965 document.querySelectorAll("wpd-context-menu.desktop-mode-my-wordpress__menu").forEach((n) => {
2966 n.dispatchEvent(new CustomEvent("tile-menu-closed"));
2967 n.remove();
2968 });
2969 }
2970 async function confirmDeleteMedia(ctx, tile, media, titleText) {
2971 const ok = await wpdConfirm({
2972 title: __("Delete media?", "desktop-mode"),
2973 message: sprintf(
2974 // translators: %s is a media item title.
2975 __("“%s” will be permanently deleted. This cannot be undone.", "desktop-mode"),
2976 titleText
2977 ),
2978 confirmLabel: __("Delete", "desktop-mode"),
2979 cancelLabel: __("Cancel", "desktop-mode")
2980 });
2981 if (!ok) {
2982 return;
2983 }
2984 try {
2985 await deleteMediaItem(media.id);
2986 removeMediaFromList(ctx, tile, media.id);
2987 showToast$1({ message: __("Media deleted.", "desktop-mode") });
2988 } catch (err) {
2989 const message = err instanceof Error ? err.message : __("Couldn’t delete that file.", "desktop-mode");
2990 showToast$1({ message });
2991 }
2992 }
2993 function removeMediaFromList(ctx, tile, mediaId) {
2994 tile.remove();
2995 if (ctx.selectedId === mediaId) {
2996 ctx.selectedId = null;
2997 ctx.selectedTile = null;
2998 ctx.preview.replaceChildren();
2999 const placeholder = document.createElement("div");
3000 placeholder.className = "desktop-mode-my-wordpress__preview-empty";
3001 placeholder.textContent = __(
3002 "Select a media item to preview it here.",
3003 "desktop-mode"
3004 );
3005 ctx.preview.appendChild(placeholder);
3006 }
3007 ctx.loaded = Math.max(0, ctx.loaded - 1);
3008 ctx.total = Math.max(0, ctx.total - 1);
3009 paintStatus$2(ctx);
3010 }
3011 function selectTile$1(ctx, tile, media) {
3012 if (ctx.selectedTile) {
3013 ctx.selectedTile.removeAttribute("selected");
3014 }
3015 tile.setAttribute("selected", "");
3016 ctx.selectedTile = tile;
3017 ctx.selectedId = media.id;
3018 const titleText = stripTags(media.title.rendered) || __("(no title)", "desktop-mode");
3019 renderMediaPreview(ctx.preview, media, {
3020 entityId: ctx.entity.id,
3021 previewActions: ctx.previewActions,
3022 onOpenDetail: () => {
3023 ctx.host.navigate({
3024 kind: "media-detail",
3025 entityId: ctx.entity.id,
3026 mediaId: media.id,
3027 mediaTitle: titleText
3028 });
3029 }
3030 });
3031 }
3032 function renderEmpty(host, message) {
3033 const empty = document.createElement("div");
3034 empty.className = "desktop-mode-my-wordpress__empty";
3035 empty.textContent = message;
3036 host.appendChild(empty);
3037 }
3038 function renderMediaList(host, entity) {
3039 const cfg = getConfig();
3040 const initialQuery = lastQueryByMediaEntity.get(entity.id) ?? "";
3041 const toolbar = renderListToolbar({
3042 placeholder: __("Search media…", "desktop-mode"),
3043 ariaLabel: __("Search media", "desktop-mode"),
3044 initialValue: initialQuery,
3045 onSearchChange: (q) => {
3046 lastQueryByMediaEntity.set(entity.id, q);
3047 void resetForSearch(q);
3048 }
3049 });
3050 host.body.appendChild(toolbar.host);
3051 host.addTeardown(() => toolbar.destroy());
3052 const split = document.createElement("div");
3053 split.className = "desktop-mode-my-wordpress__split desktop-mode-my-wordpress__split--media";
3054 const left = document.createElement("div");
3055 left.className = "desktop-mode-my-wordpress__list";
3056 const tiles = document.createElement("div");
3057 tiles.className = "desktop-mode-my-wordpress__media-grid";
3058 tiles.setAttribute("role", "list");
3059 left.appendChild(tiles);
3060 const sentinel = document.createElement("div");
3061 sentinel.className = "desktop-mode-my-wordpress__sentinel";
3062 sentinel.setAttribute("aria-hidden", "true");
3063 left.appendChild(sentinel);
3064 const right = document.createElement("div");
3065 right.className = "desktop-mode-my-wordpress__preview";
3066 const previewEmpty = document.createElement("div");
3067 previewEmpty.className = "desktop-mode-my-wordpress__preview-empty";
3068 previewEmpty.textContent = __(
3069 "Select a media item to preview it here.",
3070 "desktop-mode"
3071 );
3072 right.appendChild(previewEmpty);
3073 split.append(left, right);
3074 host.body.appendChild(split);
3075 const statusBar = host.body.closest("[data-desktop-mode-my-wordpress-root]")?.querySelector(
3076 "[data-desktop-mode-my-wordpress-status]"
3077 ) ?? document.createElement("div");
3078 const ctx = {
3079 page: 0,
3080 totalPages: 1,
3081 total: 0,
3082 loaded: 0,
3083 loading: false,
3084 done: false,
3085 tiles,
3086 sentinel,
3087 preview: right,
3088 selectedId: null,
3089 selectedTile: null,
3090 statusBar,
3091 entity,
3092 host,
3093 previewActions: cfg.previewActions ?? [],
3094 query: initialQuery,
3095 abort: null
3096 };
3097 host.addTeardown(() => ctx.abort?.abort());
3098 const sentinelIsVisible = () => {
3099 const sr = sentinel.getBoundingClientRect();
3100 const rr = left.getBoundingClientRect();
3101 const slack = 200;
3102 return sr.top < rr.bottom + slack && sr.bottom > rr.top - slack;
3103 };
3104 const loadMore = async () => {
3105 if (ctx.loading || ctx.done) {
3106 return;
3107 }
3108 ctx.loading = true;
3109 const nextPage = ctx.page + 1;
3110 const perPage = cfg.mediaPerPage ?? 48;
3111 const queryAtFetchTime = ctx.query;
3112 const controller = new AbortController();
3113 ctx.abort = controller;
3114 try {
3115 const result = await fetchMediaPage(entity, {
3116 page: nextPage,
3117 perPage,
3118 search: queryAtFetchTime || void 0,
3119 signal: controller.signal
3120 });
3121 if (ctx.query !== queryAtFetchTime) {
3122 return;
3123 }
3124 ctx.page = nextPage;
3125 ctx.totalPages = result.totalPages;
3126 ctx.total = result.total;
3127 if (result.items.length === 0 && nextPage === 1) {
3128 renderEmpty(
3129 tiles,
3130 queryAtFetchTime ? sprintf(
3131 // translators: %s is the user-entered search query.
3132 __('No media match "%s".', "desktop-mode"),
3133 queryAtFetchTime
3134 ) : __("No media yet.", "desktop-mode")
3135 );
3136 ctx.done = true;
3137 paintStatus$2(ctx);
3138 return;
3139 }
3140 for (const item of result.items) {
3141 tiles.appendChild(buildMediaTile(ctx, item));
3142 ctx.loaded += 1;
3143 }
3144 if (ctx.page >= ctx.totalPages) {
3145 ctx.done = true;
3146 }
3147 paintStatus$2(ctx);
3148 } catch (err) {
3149 if (err instanceof DOMException && err.name === "AbortError") {
3150 return;
3151 }
3152 const message = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
3153 renderEmpty(tiles, message);
3154 ctx.done = true;
3155 } finally {
3156 ctx.loading = false;
3157 if (ctx.abort === controller) {
3158 ctx.abort = null;
3159 }
3160 }
3161 if (!ctx.done) {
3162 requestAnimationFrame(() => {
3163 if (sentinelIsVisible()) {
3164 void loadMore();
3165 }
3166 });
3167 }
3168 };
3169 const resetForSearch = async (q) => {
3170 ctx.abort?.abort();
3171 ctx.abort = null;
3172 ctx.query = q;
3173 tiles.classList.add(
3174 "desktop-mode-my-wordpress__media-grid--searching"
3175 );
3176 const controller = new AbortController();
3177 ctx.abort = controller;
3178 ctx.loading = true;
3179 const perPage = cfg.mediaPerPage ?? 48;
3180 try {
3181 const result = await fetchMediaPage(entity, {
3182 page: 1,
3183 perPage,
3184 search: q || void 0,
3185 signal: controller.signal
3186 });
3187 if (ctx.query !== q) {
3188 return;
3189 }
3190 tiles.replaceChildren();
3191 tiles.classList.remove(
3192 "desktop-mode-my-wordpress__media-grid--searching"
3193 );
3194 ctx.page = 1;
3195 ctx.totalPages = result.totalPages;
3196 ctx.total = result.total;
3197 ctx.loaded = 0;
3198 ctx.done = ctx.page >= ctx.totalPages;
3199 ctx.selectedId = null;
3200 ctx.selectedTile = null;
3201 ctx.preview.replaceChildren();
3202 const emptyPreview = document.createElement("div");
3203 emptyPreview.className = "desktop-mode-my-wordpress__preview-empty";
3204 emptyPreview.textContent = __(
3205 "Select a media item to preview it here.",
3206 "desktop-mode"
3207 );
3208 ctx.preview.appendChild(emptyPreview);
3209 if (result.items.length === 0) {
3210 renderEmpty(
3211 tiles,
3212 q ? sprintf(
3213 // translators: %s is the user-entered search query.
3214 __('No media match "%s".', "desktop-mode"),
3215 q
3216 ) : __("No media yet.", "desktop-mode")
3217 );
3218 ctx.done = true;
3219 } else {
3220 for (const item of result.items) {
3221 tiles.appendChild(buildMediaTile(ctx, item));
3222 ctx.loaded += 1;
3223 }
3224 }
3225 paintStatus$2(ctx);
3226 } catch (err) {
3227 if (err instanceof DOMException && err.name === "AbortError") {
3228 return;
3229 }
3230 tiles.classList.remove(
3231 "desktop-mode-my-wordpress__media-grid--searching"
3232 );
3233 tiles.replaceChildren();
3234 const message = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
3235 renderEmpty(tiles, message);
3236 ctx.done = true;
3237 } finally {
3238 ctx.loading = false;
3239 if (ctx.abort === controller) {
3240 ctx.abort = null;
3241 }
3242 }
3243 if (!ctx.done) {
3244 requestAnimationFrame(() => {
3245 if (sentinelIsVisible()) {
3246 void loadMore();
3247 }
3248 });
3249 }
3250 };
3251 if (typeof IntersectionObserver !== "undefined") {
3252 const observer = new IntersectionObserver(
3253 (entries) => {
3254 for (const e of entries) {
3255 if (e.isIntersecting) {
3256 void loadMore();
3257 }
3258 }
3259 },
3260 { root: left, rootMargin: "200px 0px" }
3261 );
3262 observer.observe(sentinel);
3263 host.addTeardown(() => observer.disconnect());
3264 } else {
3265 const onScroll = () => {
3266 if (sentinelIsVisible()) {
3267 void loadMore();
3268 }
3269 };
3270 left.addEventListener("scroll", onScroll, { passive: true });
3271 host.addTeardown(() => left.removeEventListener("scroll", onScroll));
3272 }
3273 const liveNs = `desktop-mode/my-wordpress-media-live-${Math.random().toString(36).slice(2, 8)}`;
3274 addAction(FILE_DROP_HOOKS.AFTER_UPLOAD, liveNs, (payload) => {
3275 void spliceNewMedia(ctx, payload.result.id);
3276 });
3277 host.addTeardown(
3278 () => removeAction(FILE_DROP_HOOKS.AFTER_UPLOAD, liveNs)
3279 );
3280 host.addTeardown(() => closeAnyMediaTileMenu());
3281 paintStatus$2(ctx);
3282 void loadMore();
3283 }
3284 async function spliceNewMedia(ctx, mediaId) {
3285 if (!ctx.tiles.isConnected) {
3286 return;
3287 }
3288 if (ctx.tiles.querySelector(
3289 `wpd-tile[data-media-id="${mediaId}"]`
3290 )) {
3291 return;
3292 }
3293 try {
3294 const item = await fetchMediaItem(mediaId);
3295 ctx.tiles.insertBefore(buildMediaTile(ctx, item), ctx.tiles.firstChild);
3296 ctx.loaded += 1;
3297 ctx.total += 1;
3298 paintStatus$2(ctx);
3299 } catch (err) {
3300 console.warn(
3301 "[my-wordpress/media] live-refresh fetch failed:",
3302 err
3303 );
3304 }
3305 }
3306 function entityForPostType(postType) {
3307 const suffix = "/" + postType;
3308 return getConfig().entities.find((e) => e.restPath.endsWith(suffix));
3309 }
3310 function entityIdForPostType(postType) {
3311 const match = entityForPostType(postType);
3312 if (match) {
3313 return match.id;
3314 }
3315 if (postType === "page") {
3316 return "pages";
3317 }
3318 return "posts";
3319 }
3320 function entityIconForPostType(postType) {
3321 const match = entityForPostType(postType);
3322 if (match) {
3323 return match.icon;
3324 }
3325 if (postType === "page") {
3326 return "dashicons-admin-page";
3327 }
3328 return "dashicons-admin-post";
3329 }
3330 function openDetailInWindow(payload) {
3331 const myWp = window.wp?.desktop?.myWordpress;
3332 myWp?.openDetail?.(payload);
3333 }
3334 function buildUsageTile(row) {
3335 const titleText = row.title || `#${row.postId}`;
3336 const tile = buildTileFromSpec({
3337 type: "post",
3338 ref: String(row.postId),
3339 label: titleText,
3340 icon: entityIconForPostType(row.postType),
3341 role: "entry",
3342 status: row.status,
3343 dataset: { postId: row.postId, postType: row.postType },
3344 extraClasses: [
3345 "desktop-mode-my-wordpress__tile",
3346 "desktop-mode-my-wordpress__tile--entry",
3347 "desktop-mode-my-wordpress__media-tile",
3348 "desktop-mode-my-wordpress__tile--usage"
3349 ]
3350 });
3351 attachTileDragOut(tile, {
3352 kind: "post",
3353 ref: String(row.postId),
3354 title: titleText,
3355 icon: entityIconForPostType(row.postType)
3356 });
3357 return tile;
3358 }
3359 let openContextMenu = null;
3360 function closeContextMenu() {
3361 if (openContextMenu && openContextMenu.isConnected) {
3362 openContextMenu.remove();
3363 }
3364 openContextMenu = null;
3365 }
3366 function openUsageTileMenu(row, pos) {
3367 closeContextMenu();
3368 const menu = document.createElement("wpd-context-menu");
3369 menu.setAttribute("open", "");
3370 menu.classList.add("desktop-mode-my-wordpress__menu");
3371 menu.style.left = `${pos.x}px`;
3372 menu.style.top = `${pos.y}px`;
3373 const addOption = (id, label, icon) => {
3374 const opt = document.createElement("wpd-context-menu-option");
3375 opt.dataset.menuItemId = id;
3376 opt.setAttribute("value", id);
3377 opt.setAttribute("icon", icon);
3378 opt.textContent = label;
3379 menu.appendChild(opt);
3380 };
3381 addOption("navigate-into", __("Open in My WordPress", "desktop-mode"), "dashicons-category");
3382 if (row.editLink) {
3383 addOption("open-editor", __("Open in editor", "desktop-mode"), "dashicons-edit");
3384 }
3385 if (row.link) {
3386 addOption("open-front", __("View on site", "desktop-mode"), "dashicons-external");
3387 }
3388 menu.addEventListener("wpd-context-menu-pick", (e) => {
3389 const detail = e.detail;
3390 closeContextMenu();
3391 if (detail.id === "navigate-into") {
3392 openDetailInWindow({
3393 entityId: entityIdForPostType(row.postType),
3394 postId: row.postId,
3395 postTitle: row.title
3396 });
3397 return;
3398 }
3399 if (detail.id === "open-editor" && row.editLink) {
3400 window.open(row.editLink, "_blank", "noopener,noreferrer");
3401 return;
3402 }
3403 if (detail.id === "open-front" && row.link) {
3404 window.open(row.link, "_blank", "noopener,noreferrer");
3405 }
3406 });
3407 document.body.appendChild(menu);
3408 openContextMenu = menu;
3409 const rect = menu.getBoundingClientRect();
3410 if (rect.right > window.innerWidth) {
3411 menu.style.left = `${Math.max(
3412 0,
3413 window.innerWidth - rect.width - 8
3414 )}px`;
3415 }
3416 if (rect.bottom > window.innerHeight) {
3417 menu.style.top = `${Math.max(
3418 0,
3419 window.innerHeight - rect.height - 8
3420 )}px`;
3421 }
3422 queueMicrotask(() => {
3423 const onDoc = (ev) => {
3424 const target = ev.target;
3425 if (target instanceof Node && menu.contains(target)) {
3426 return;
3427 }
3428 closeContextMenu();
3429 document.removeEventListener("pointerdown", onDoc, true);
3430 document.removeEventListener("keydown", onKey);
3431 };
3432 const onKey = (ev) => {
3433 if (ev.key === "Escape") {
3434 closeContextMenu();
3435 document.removeEventListener("pointerdown", onDoc, true);
3436 document.removeEventListener("keydown", onKey);
3437 }
3438 };
3439 document.addEventListener("pointerdown", onDoc, true);
3440 document.addEventListener("keydown", onKey);
3441 });
3442 }
3443 function paintStatus$1(statusBar, count, entityId) {
3444 const segments = [
3445 {
3446 id: "count",
3447 label: sprintf(
3448 // translators: %d is the count of posts that reference an attachment.
3449 _n("%d reference", "%d references", count),
3450 count
3451 ),
3452 align: "start",
3453 sort: 10
3454 }
3455 ];
3456 const filtered = applyFilters(
3457 "desktop-mode.my-wordpress.status-bar",
3458 segments,
3459 { view: "media-detail", entityId }
3460 );
3461 renderStatusBarSegments(
3462 statusBar,
3463 Array.isArray(filtered) ? filtered : segments
3464 );
3465 }
3466 async function renderMediaDetail(host, mediaId) {
3467 const wrap = document.createElement("div");
3468 wrap.className = "desktop-mode-my-wordpress__split desktop-mode-my-wordpress__split--media-detail";
3469 const left = document.createElement("div");
3470 left.className = "desktop-mode-my-wordpress__list desktop-mode-my-wordpress__usage-list";
3471 const loading = document.createElement("div");
3472 loading.className = "desktop-mode-my-wordpress__preview-loading";
3473 const spinner = document.createElement("wpd-spinner");
3474 loading.appendChild(spinner);
3475 left.appendChild(loading);
3476 const right = document.createElement("div");
3477 right.className = "desktop-mode-my-wordpress__preview";
3478 wrap.append(left, right);
3479 host.body.appendChild(wrap);
3480 const statusBar = host.body.closest("[data-desktop-mode-my-wordpress-root]")?.querySelector(
3481 "[data-desktop-mode-my-wordpress-status]"
3482 ) ?? document.createElement("div");
3483 let usage;
3484 try {
3485 usage = await fetchMediaUsage(mediaId);
3486 } catch (err) {
3487 if (!wrap.isConnected) {
3488 return;
3489 }
3490 left.replaceChildren();
3491 const errBox = document.createElement("div");
3492 errBox.className = "desktop-mode-my-wordpress__error";
3493 errBox.textContent = err instanceof Error ? err.message : __("Failed to load usage data.", "desktop-mode");
3494 left.appendChild(errBox);
3495 return;
3496 }
3497 if (!wrap.isConnected) {
3498 return;
3499 }
3500 if (host.route.kind !== "media-detail") {
3501 throw new Error(
3502 "[my-wordpress] renderMediaDetail invoked outside a media-detail route."
3503 );
3504 }
3505 const entityId = host.route.entityId;
3506 const summary = document.createElement("div");
3507 summary.className = "desktop-mode-my-wordpress__media-detail-summary-bar";
3508 const summaryText = document.createElement("p");
3509 summaryText.className = "desktop-mode-my-wordpress__media-detail-summary";
3510 summaryText.textContent = sprintf(
3511 // translators: %d is the count of posts/pages referencing this file.
3512 _n(
3513 "%d entry references this file.",
3514 "%d entries reference this file.",
3515 usage.usedIn.length
3516 ),
3517 usage.usedIn.length
3518 );
3519 summary.appendChild(summaryText);
3520 right.replaceChildren(summary);
3521 const mediaItem = {
3522 id: usage.media.id,
3523 title: { rendered: usage.media.title },
3524 date: usage.media.date,
3525 mime_type: usage.media.mime,
3526 source_url: usage.media.sourceUrl,
3527 media_details: {
3528 file: usage.media.filename
3529 },
3530 _embedded: usage.media.author.name ? { author: [{ id: usage.media.author.id, name: usage.media.author.name }] } : void 0
3531 };
3532 const previewHost = document.createElement("div");
3533 previewHost.className = "desktop-mode-my-wordpress__media-detail-preview";
3534 renderMediaPreview(previewHost, mediaItem, {
3535 entityId,
3536 previewActions: getConfig().previewActions ?? []
3537 });
3538 right.appendChild(previewHost);
3539 left.replaceChildren();
3540 if (usage.usedIn.length === 0) {
3541 const empty = document.createElement("div");
3542 empty.className = "desktop-mode-my-wordpress__empty";
3543 empty.textContent = __(
3544 "No posts or pages reference this file.",
3545 "desktop-mode"
3546 );
3547 left.appendChild(empty);
3548 paintStatus$1(statusBar, 0, entityId);
3549 return;
3550 }
3551 const grid = document.createElement("div");
3552 grid.className = "desktop-mode-my-wordpress__media-grid desktop-mode-my-wordpress__usage-grid";
3553 grid.setAttribute("role", "list");
3554 for (const row of usage.usedIn) {
3555 const tile = buildUsageTile(row);
3556 tile.addEventListener("dblclick", (e) => {
3557 e.preventDefault();
3558 openDetailInWindow({
3559 entityId: entityIdForPostType(row.postType),
3560 postId: row.postId,
3561 postTitle: row.title
3562 });
3563 });
3564 tile.addEventListener("contextmenu", (e) => {
3565 e.preventDefault();
3566 openUsageTileMenu(row, { x: e.clientX, y: e.clientY });
3567 });
3568 grid.appendChild(tile);
3569 }
3570 left.appendChild(grid);
3571 host.addTeardown(closeContextMenu);
3572 paintStatus$1(statusBar, usage.usedIn.length, entityId);
3573 }
3574 const ROOT_CLASS = "desktop-mode-breadcrumbs";
3575 function renderBreadcrumbs(host, segments, opts = {}) {
3576 host.replaceChildren();
3577 host.classList.add(ROOT_CLASS);
3578 if (opts.onBack) {
3579 const back = document.createElement("button");
3580 back.type = "button";
3581 back.className = `${ROOT_CLASS}__back`;
3582 back.setAttribute("aria-label", __("Back", "desktop-mode"));
3583 back.title = __("Back", "desktop-mode");
3584 const arrow = document.createElement("span");
3585 arrow.className = "dashicons dashicons-arrow-left-alt2";
3586 arrow.setAttribute("aria-hidden", "true");
3587 back.appendChild(arrow);
3588 if (opts.backDisabled) {
3589 back.disabled = true;
3590 }
3591 const onBack = opts.onBack;
3592 back.addEventListener("click", () => {
3593 if (back.disabled) {
3594 return;
3595 }
3596 onBack();
3597 });
3598 host.appendChild(back);
3599 }
3600 const nav = document.createElement("nav");
3601 nav.className = `${ROOT_CLASS}__crumbs`;
3602 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
3603 segments.forEach((seg, idx) => {
3604 if (idx > 0) {
3605 const sep = document.createElement("span");
3606 sep.className = `${ROOT_CLASS}__sep`;
3607 sep.setAttribute("aria-hidden", "true");
3608 sep.textContent = "";
3609 nav.appendChild(sep);
3610 }
3611 if (!seg.onClick) {
3612 const here = document.createElement("span");
3613 here.className = `${ROOT_CLASS}__crumb ${ROOT_CLASS}__crumb--current`;
3614 here.setAttribute("aria-current", "page");
3615 here.textContent = seg.label;
3616 nav.appendChild(here);
3617 return;
3618 }
3619 const btn = document.createElement("button");
3620 btn.type = "button";
3621 btn.className = `${ROOT_CLASS}__crumb`;
3622 btn.textContent = seg.label;
3623 const onClick = seg.onClick;
3624 btn.addEventListener("click", () => {
3625 onClick();
3626 });
3627 nav.appendChild(btn);
3628 });
3629 host.appendChild(nav);
3630 }
3631 const styles$1 = 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}`;
3632 const _WpdButton = class _WpdButton extends Component {
3633 render() {
3634 const disabled = this.disabled !== null;
3635 const type = this.type || "button";
3636 return html`
3637 <button part="button" type=${type} ?disabled=${disabled}>
3638 <slot></slot>
3639 </button>
3640 `;
3641 }
3642 };
3643 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
3644 _WpdButton.styles = [styles$1];
3645 _WpdButton.help = {
3646 title: "Button",
3647 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
3648 status: "stable",
3649 since: "0.9.0",
3650 props: [
3651 {
3652 name: "variant",
3653 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
3654 default: "ghost",
3655 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
3656 },
3657 {
3658 name: "disabled",
3659 type: "boolean attribute",
3660 description: "Disable pointer + keyboard interaction and dim the chrome."
3661 },
3662 {
3663 name: "type",
3664 type: "'button' | 'submit' | 'reset'",
3665 default: "button",
3666 description: "Forwarded to the underlying native <button>."
3667 },
3668 {
3669 name: "busy",
3670 type: "boolean attribute",
3671 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
3672 },
3673 {
3674 name: "fill-cell",
3675 type: "boolean attribute",
3676 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
3677 }
3678 ],
3679 slots: [{ name: "(default)", description: "Button label." }],
3680 parts: [{ name: "button", description: "Underlying <button> element." }],
3681 cssProps: [
3682 { name: "--wpd-button-bg", description: "Background color." },
3683 { name: "--wpd-button-fg", description: "Text color." },
3684 { name: "--wpd-button-border", description: "Border shorthand." },
3685 { name: "--wpd-button-border-radius", default: "6px" },
3686 { name: "--wpd-button-padding", default: "6px 12px" },
3687 {
3688 name: "--wpd-button-min-height",
3689 description: "Minimum height when fill-cell is set."
3690 }
3691 ],
3692 example: html`
3693 <wpd-cluster gap="8">
3694 <wpd-button variant="primary">Primary</wpd-button>
3695 <wpd-button variant="secondary">Secondary</wpd-button>
3696 <wpd-button variant="ghost">Ghost</wpd-button>
3697 <wpd-button variant="danger">Danger</wpd-button>
3698 <wpd-button variant="link">Link</wpd-button>
3699 </wpd-cluster>
3700 `
3701 };
3702 let WpdButton = _WpdButton;
3703 defineComponent("wpd-button", WpdButton);
3704 const menuStyles = css`:host{display:none;position:fixed;min-width:180px;background:var( --wpd-context-menu-bg,var( --desktop-mode-bg,#1d2327 ) );color:var( --wpd-context-menu-fg,var( --desktop-mode-fg,#fff ) );border:1px solid rgba( 255,255,255,0.08 );border-radius:8px;box-shadow:0 8px 24px rgba( 0,0,0,0.45 );padding:4px;font-size:13px;line-height:1.3;z-index:9999}:host( [ open ] ){display:block}`;
3705 const optionStyles = css`:host{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border:0;background:transparent;color:inherit;text-align:start;cursor:pointer;border-radius:4px;box-sizing:border-box;user-select:none}:host(:hover ),:host( [ active ] ){background:rgba( 255,255,255,0.1 );outline:none}:host( [ disabled ] ){opacity:0.45;cursor:not-allowed}:host( [ danger ] ){color:#ff8a8a}:host( [ danger ]:hover ){background:rgba( 255,90,90,0.18 )}:host( [ heading ] ){padding:8px 10px 4px;font-size:10px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:var( --wpd-context-menu-fg-muted,rgba( 255,255,255,0.5 ) );pointer-events:none}.icon{display:inline-flex;align-items:center;justify-content:center;font-size:18px;width:20px;height:20px}.label{flex:1}.chevron{margin-inline-start:auto;padding-inline-start:8px;font-size:16px;line-height:1;opacity:0.7}.check{display:inline-flex;align-items:center;justify-content:center;width:14px;font-size:13px;line-height:1;opacity:0.95}`;
3706 const _WpdContextMenu = class _WpdContextMenu extends Component {
3707 render() {
3708 return html`
3709 <slot></slot>
3710 `;
3711 }
3712 connectedCallback() {
3713 super.connectedCallback();
3714 this.setAttribute("role", "menu");
3715 }
3716 };
3717 _WpdContextMenu.props = ["open"];
3718 _WpdContextMenu.styles = [menuStyles];
3719 _WpdContextMenu.help = {
3720 title: "Context menu",
3721 summary: "Floating popup menu primitive. Pair with <wpd-context-menu-option> children. Toggle via the `open` boolean attribute. Listen for `wpd-context-menu-pick` to handle activation.",
3722 status: "experimental",
3723 since: "0.9.0",
3724 props: [
3725 {
3726 name: "open",
3727 type: "boolean attribute",
3728 description: "Mounts the menu in its open / visible state."
3729 }
3730 ],
3731 slots: [
3732 { name: "(default)", description: "List of <wpd-context-menu-option> items." }
3733 ],
3734 events: [
3735 {
3736 name: "wpd-context-menu-pick",
3737 description: "Bubbled from a non-disabled, non-heading option on activation. Detail: `{ id, value }`."
3738 }
3739 ]
3740 };
3741 let WpdContextMenu = _WpdContextMenu;
3742 defineComponent("wpd-context-menu", WpdContextMenu);
3743 const _WpdContextMenuOption = class _WpdContextMenuOption extends Component {
3744 constructor() {
3745 super(...arguments);
3746 this._onActivate = (e) => {
3747 if (this.hasAttribute("disabled") || this.hasAttribute("heading")) {
3748 return;
3749 }
3750 const target = e.target;
3751 if (target && target !== this && target.closest("wpd-context-menu-option") !== this) {
3752 return;
3753 }
3754 this.emit("wpd-context-menu-pick", {
3755 id: this.dataset.menuItemId ?? this.id ?? "",
3756 value: this.getAttribute("value") ?? ""
3757 });
3758 };
3759 this._onKey = (e) => {
3760 if (e.key === "Enter" || e.key === " ") {
3761 e.preventDefault();
3762 this._onActivate(e);
3763 }
3764 };
3765 }
3766 connectedCallback() {
3767 super.connectedCallback();
3768 const isHeading = this.hasAttribute("heading");
3769 this.setAttribute("role", isHeading ? "presentation" : "menuitem");
3770 if (!isHeading) {
3771 this.setAttribute("tabindex", "0");
3772 }
3773 this.addEventListener("click", this._onActivate);
3774 this.addEventListener("keydown", this._onKey);
3775 }
3776 disconnectedCallback() {
3777 this.removeEventListener("click", this._onActivate);
3778 this.removeEventListener("keydown", this._onKey);
3779 }
3780 render() {
3781 const icon = this.getAttribute("icon");
3782 const hasChildren2 = this.hasAttribute("has-children");
3783 const checked = this.hasAttribute("checked");
3784 return html`
3785 ${checked ? html`<span class="check" aria-hidden="true">✓</span>` : html``}
3786 ${icon ? html`<span class="icon dashicons ${icon}" aria-hidden="true"></span>` : html``}
3787 <span class="label"><slot></slot></span>
3788 ${hasChildren2 ? html`<span class="chevron" aria-hidden="true">›</span>` : html``}
3789 `;
3790 }
3791 };
3792 _WpdContextMenuOption.props = [
3793 "value",
3794 "icon",
3795 "disabled",
3796 "danger",
3797 "heading",
3798 "has-children",
3799 "checked"
3800 ];
3801 _WpdContextMenuOption.styles = [optionStyles];
3802 _WpdContextMenuOption.help = {
3803 title: "Context menu option",
3804 summary: "Single row inside <wpd-context-menu>. Use `icon` for a leading dashicon, `danger` for destructive items, `heading` for a non-interactive section header, `has-children` to render a trailing chevron.",
3805 status: "experimental",
3806 since: "0.9.0",
3807 props: [
3808 {
3809 name: "value",
3810 type: "string",
3811 description: "Forwarded as `detail.value` on activation."
3812 },
3813 {
3814 name: "icon",
3815 type: "string",
3816 description: "Dashicon class (e.g. `dashicons-trash`)."
3817 },
3818 {
3819 name: "disabled",
3820 type: "boolean attribute",
3821 description: "Renders the option dimmed; clicks are ignored."
3822 },
3823 {
3824 name: "danger",
3825 type: "boolean attribute",
3826 description: "Destructive styling — red text, red hover."
3827 },
3828 {
3829 name: "heading",
3830 type: "boolean attribute",
3831 description: "Non-interactive section header. Ignores clicks."
3832 },
3833 {
3834 name: "has-children",
3835 type: "boolean attribute",
3836 description: "Renders a trailing chevron to suggest a submenu."
3837 },
3838 {
3839 name: "checked",
3840 type: "boolean attribute",
3841 description: "Renders a leading check mark — for radio-style picks inside a submenu (e.g. the active Sort By order)."
3842 }
3843 ],
3844 slots: [
3845 { name: "(default)", description: "Visible label + optional nested <wpd-context-menu>." }
3846 ],
3847 events: [
3848 {
3849 name: "wpd-context-menu-pick",
3850 description: "Bubbled on click / Enter for non-heading non-disabled options. Detail: `{ id, value }`."
3851 }
3852 ]
3853 };
3854 let WpdContextMenuOption = _WpdContextMenuOption;
3855 defineComponent("wpd-context-menu-option", WpdContextMenuOption);
3856 const styles = css`:host{display:inline-block;--wpd-spinner-color:var( --wp-admin-theme-color,#21759b );--wpd-spinner-accent:#fff;--wpd-spinner-size:48px;width:var( --wpd-spinner-size );height:var( --wpd-spinner-size );color:var( --wpd-spinner-color );vertical-align:middle;line-height:0}:host( [ hidden ] ){display:none}.root,.root svg{display:block;width:100%;height:100%}.root svg .mark{fill:var( --wpd-spinner-accent,#fff )}@keyframes wpd-spinner-spin{to{transform:rotate( 360deg )}}@keyframes wpd-spinner-scale{0%,100%{transform:scale( 1 )}50%{transform:scale( 1.045 )}}@keyframes wpd-spinner-opacity{0%,100%{opacity:1}50%{opacity:0.7}}@media ( prefers-reduced-motion:reduce ){.root svg [ style*='animation' ]{animation:none !important}}`;
3857 const WPD_SPINNER_PRESETS = Object.freeze({
3858 classic: {
3859 sp1: 12,
3860 sp2: 24,
3861 sp3: 40,
3862 a1: 28,
3863 a2: 15,
3864 a3: 8,
3865 gap: 4,
3866 dir2: 1,
3867 dir3: -1,
3868 pulse: "none",
3869 dots: 0
3870 },
3871 comet: {
3872 sp1: 8,
3873 sp2: 14,
3874 sp3: 26,
3875 a1: 50,
3876 a2: 28,
3877 a3: 12,
3878 gap: 3,
3879 dir2: 1,
3880 dir3: 1,
3881 pulse: "none",
3882 dots: 5
3883 },
3884 orbit: {
3885 sp1: 10,
3886 sp2: 10,
3887 sp3: 32,
3888 a1: 50,
3889 a2: 50,
3890 a3: 8,
3891 gap: 5,
3892 dir2: -1,
3893 dir3: -1,
3894 pulse: "opacity",
3895 dots: 3
3896 },
3897 pulse: {
3898 sp1: 6,
3899 sp2: 18,
3900 sp3: 30,
3901 a1: 20,
3902 a2: 12,
3903 a3: 6,
3904 gap: 4,
3905 dir2: 1,
3906 dir3: -1,
3907 pulse: "both",
3908 dots: 8
3909 }
3910 });
3911 const CX = 61.26;
3912 const CY = 61.26;
3913 const DISC_R = 58.453;
3914 const W_PATHS = '<path d="m8.708 61.26c0 20.802 12.089 38.779 29.619 47.298l-25.069-68.686c-2.916 6.536-4.55 13.769-4.55 21.388z"/><path d="m96.74 58.608c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.501-34.493-8.188-22.434c-2.83-.166-5.511-.501-5.511-.501-2.832-.166-2.5-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.853.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989z"/><path d="m62.184 65.857-15.768 45.819c4.708 1.384 9.687 2.141 14.846 2.141 6.12 0 11.989-1.058 17.452-2.979-.141-.225-.269-.464-.374-.724z"/><path d="m107.376 36.046c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215z"/>';
3915 const _WpdSpinner = class _WpdSpinner extends Component {
3916 constructor() {
3917 super(...arguments);
3918 this._paintScheduled = false;
3919 }
3920 connectedCallback() {
3921 super.connectedCallback();
3922 this._schedulePaint();
3923 }
3924 render() {
3925 return html`<div class="root" part="root"></div>`;
3926 }
3927 requestUpdate() {
3928 super.requestUpdate();
3929 this._schedulePaint();
3930 }
3931 _schedulePaint() {
3932 if (this._paintScheduled || !this.isConnected) {
3933 return;
3934 }
3935 this._paintScheduled = true;
3936 queueMicrotask(() => {
3937 this._paintScheduled = false;
3938 if (!this.isConnected) {
3939 return;
3940 }
3941 this._paint();
3942 });
3943 }
3944 _paint() {
3945 this._syncCssVars();
3946 const root = this.shadowRoot?.querySelector(
3947 ".root"
3948 );
3949 if (!root) {
3950 return;
3951 }
3952 root.innerHTML = this._buildSvg();
3953 }
3954 /**
3955 * Reflect the color / accent / size attributes onto CSS custom
3956 * properties on the host. Removing the attribute clears the var
3957 * so the default cascades back in.
3958 */
3959 _syncCssVars() {
3960 const sync = (attr, varName, transform) => {
3961 const v = this.getAttribute(attr);
3962 if (v === null) {
3963 this.style.removeProperty(varName);
3964 } else {
3965 this.style.setProperty(
3966 varName,
3967 transform ? transform(v) : v
3968 );
3969 }
3970 };
3971 sync("color", "--wpd-spinner-color");
3972 sync("accent", "--wpd-spinner-accent");
3973 sync(
3974 "size",
3975 "--wpd-spinner-size",
3976 (v) => /^-?\d+(\.\d+)?$/.test(v.trim()) ? `${v}px` : v
3977 );
3978 }
3979 _effectiveConfig() {
3980 const presetName = this.getAttribute("preset") ?? "classic";
3981 const preset = WPD_SPINNER_PRESETS[presetName] ?? WPD_SPINNER_PRESETS.classic;
3982 const num = (attr, fallback) => {
3983 const v = this.getAttribute(attr);
3984 if (v === null) {
3985 return fallback;
3986 }
3987 const n = parseFloat(v);
3988 return Number.isFinite(n) ? n : fallback;
3989 };
3990 const dir = (attr, fallback) => {
3991 const v = this.getAttribute(attr);
3992 if (v === null) {
3993 return fallback;
3994 }
3995 const lc = v.toLowerCase();
3996 if (lc === "-1" || lc === "ccw" || lc === "reverse") {
3997 return -1;
3998 }
3999 return 1;
4000 };
4001 const pulse = () => {
4002 const v = this.getAttribute("pulse");
4003 if (v === "scale" || v === "opacity" || v === "both" || v === "none") {
4004 return v;
4005 }
4006 return preset.pulse;
4007 };
4008 return {
4009 sp1: num("sp1", preset.sp1),
4010 sp2: num("sp2", preset.sp2),
4011 sp3: num("sp3", preset.sp3),
4012 a1: num("a1", preset.a1),
4013 a2: num("a2", preset.a2),
4014 a3: num("a3", preset.a3),
4015 gap: num("gap", preset.gap),
4016 dir2: dir("dir2", preset.dir2),
4017 dir3: dir("dir3", preset.dir3),
4018 pulse: pulse(),
4019 dots: Math.max(0, Math.floor(num("dots", preset.dots)))
4020 };
4021 }
4022 _buildSvg() {
4023 const cfg = this._effectiveConfig();
4024 const label = escAttr(this.getAttribute("label") ?? "Loading");
4025 const pad = cfg.gap * 3 + 14;
4026 const vbMin = -pad;
4027 const vbSize = 122.52 + pad * 2;
4028 const r1 = DISC_R + cfg.gap + 2;
4029 const r2 = r1 + cfg.gap + 2;
4030 const r3 = r2 + cfg.gap + 1.5;
4031 const ring1Anim = `animation: wpd-spinner-spin ${(cfg.sp1 / 10).toFixed(2)}s linear infinite`;
4032 const ring2Anim = `animation: wpd-spinner-spin ${(cfg.sp2 / 10).toFixed(2)}s linear infinite${cfg.dir2 < 0 ? " reverse" : ""}`;
4033 const ring3Anim = `animation: wpd-spinner-spin ${(cfg.sp3 / 10).toFixed(2)}s linear infinite${cfg.dir3 < 0 ? " reverse" : ""}`;
4034 const pspd = (cfg.sp1 * 1.8 / 10).toFixed(1);
4035 const ospd = (cfg.sp1 * 2.3 / 10).toFixed(1);
4036 let pulseStyle = "";
4037 if (cfg.pulse === "scale") {
4038 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite`;
4039 } else if (cfg.pulse === "opacity") {
4040 pulseStyle = `animation: wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
4041 } else if (cfg.pulse === "both") {
4042 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite, wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
4043 }
4044 let dotEls = "";
4045 if (cfg.dots > 0) {
4046 const dr = r3 + cfg.gap + 1;
4047 const dc2 = 2 * Math.PI * dr;
4048 const dsz = 1.6;
4049 const dotDur = (cfg.sp1 * 0.65 / 10).toFixed(2);
4050 for (let i = 0; i < cfg.dots; i++) {
4051 const offset = -(i / cfg.dots) * dc2;
4052 dotEls += `<circle cx="${CX}" cy="${CY}" r="${dr.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="${dsz}" stroke-dasharray="${dsz.toFixed(2)} ${(dc2 - dsz).toFixed(2)}" stroke-dashoffset="${offset.toFixed(2)}" stroke-linecap="round" stroke-opacity="0.65" style="transform-origin:${CX}px ${CY}px;animation: wpd-spinner-spin ${dotDur}s linear infinite"/>`;
4053 }
4054 }
4055 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vbMin} ${vbMin} ${vbSize} ${vbSize}" role="img" aria-label="${label}"><g style="transform-origin:${CX}px ${CY}px${pulseStyle ? ";" + pulseStyle : ""}"><circle cx="${CX}" cy="${CY}" r="${DISC_R}" fill="currentColor"/><g class="mark">${W_PATHS}</g></g><circle cx="${CX}" cy="${CY}" r="${r1.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="0.6" stroke-opacity="0.2"/><circle cx="${CX}" cy="${CY}" r="${r1.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="2.2" stroke-dasharray="${dasharray(r1, cfg.a1)}" stroke-linecap="round" style="transform-origin:${CX}px ${CY}px;${ring1Anim}"/><circle cx="${CX}" cy="${CY}" r="${r2.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="0.5" stroke-opacity="0.15"/><circle cx="${CX}" cy="${CY}" r="${r2.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.8" stroke-dasharray="${dasharray(r2, cfg.a2)}" stroke-linecap="round" style="transform-origin:${CX}px ${CY}px;${ring2Anim}"/><circle cx="${CX}" cy="${CY}" r="${r3.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="0.4" stroke-opacity="0.12"/><circle cx="${CX}" cy="${CY}" r="${r3.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="1.0" stroke-opacity="0.6" stroke-dasharray="${dasharray(r3, cfg.a3)}" stroke-linecap="round" style="transform-origin:${CX}px ${CY}px;${ring3Anim}"/>` + dotEls + `</svg>`;
4056 }
4057 };
4058 _WpdSpinner.props = [
4059 "preset",
4060 "size",
4061 "color",
4062 "accent",
4063 "sp1",
4064 "sp2",
4065 "sp3",
4066 "a1",
4067 "a2",
4068 "a3",
4069 "gap",
4070 "dir2",
4071 "dir3",
4072 "pulse",
4073 "dots",
4074 "label"
4075 ];
4076 _WpdSpinner.styles = [styles];
4077 _WpdSpinner.help = {
4078 title: "Spinner",
4079 summary: "Animated WordPress-mark loading indicator with four curated presets and full per-attribute overrides. CSS variables drive disc + accent colors and size; reduced-motion preferences are respected.",
4080 status: "experimental",
4081 since: "0.18.0",
4082 props: [
4083 {
4084 name: "preset",
4085 type: '"classic" | "comet" | "orbit" | "pulse"',
4086 default: "classic",
4087 description: "Visual personality. Every other attribute defaults to the preset's value and can be overridden individually."
4088 },
4089 {
4090 name: "size",
4091 type: "integer (px) or CSS length",
4092 default: "48",
4093 description: "Sets `--wpd-spinner-size`. Bare numbers are treated as px; pass a CSS length (e.g. `2em`) to opt into ems / rems."
4094 },
4095 {
4096 name: "color",
4097 type: "CSS color",
4098 description: "Disc + ring + dot color. Sets `--wpd-spinner-color`. Default inherits the WP admin theme color."
4099 },
4100 {
4101 name: "accent",
4102 type: "CSS color",
4103 default: "#fff",
4104 description: "Color of the W mark inside the disc. Sets `--wpd-spinner-accent`. Default white — change for dark-on-light or themed marks."
4105 },
4106 {
4107 name: "sp1, sp2, sp3",
4108 type: "integer (deciseconds)",
4109 description: "Per-ring rotation duration in tenths-of-a-second (12 → 1.2s). Higher = slower."
4110 },
4111 {
4112 name: "a1, a2, a3",
4113 type: "integer (0-100)",
4114 description: "Per-ring arc length as a percentage of the ring circumference."
4115 },
4116 {
4117 name: "gap",
4118 type: "integer",
4119 description: "Gap between concentric rings (units approximate to px at 120-viewport)."
4120 },
4121 {
4122 name: "dir2, dir3",
4123 type: '"1" | "-1" | "cw" | "ccw"',
4124 description: "Per-ring direction; ring 1 is always clockwise."
4125 },
4126 {
4127 name: "pulse",
4128 type: '"none" | "scale" | "opacity" | "both"',
4129 description: "Pulse animation applied to the disc + W mark."
4130 },
4131 {
4132 name: "dots",
4133 type: "integer",
4134 description: "Outer trailing dot count. Sensible values: 0, 3, 5, 8."
4135 },
4136 {
4137 name: "label",
4138 type: "string",
4139 default: "Loading",
4140 description: 'Accessible name for the SVG (`role="img"` + `aria-label`).'
4141 }
4142 ],
4143 cssProps: [
4144 { name: "--wpd-spinner-color", default: "var(--wp-admin-theme-color, #21759b)" },
4145 { name: "--wpd-spinner-accent", default: "#fff" },
4146 { name: "--wpd-spinner-size", default: "48px" }
4147 ],
4148 example: html`<wpd-spinner preset="comet" size="80"></wpd-spinner>`
4149 };
4150 let WpdSpinner = _WpdSpinner;
4151 function dasharray(r, pct) {
4152 const c = 2 * Math.PI * r;
4153 const visible = pct / 100 * c;
4154 const gap = c - visible;
4155 return `${visible.toFixed(2)} ${gap.toFixed(2)}`;
4156 }
4157 function escAttr(s) {
4158 return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4159 }
4160 defineComponent("wpd-spinner", WpdSpinner);
4161 const WINDOW_ID = "desktop-mode-my-wordpress";
4162 const ROOT_SEL = "[data-desktop-mode-my-wordpress-root]";
4163 const BREADCRUMBS_SEL = "[data-desktop-mode-my-wordpress-breadcrumbs]";
4164 const BODY_SEL = "[data-desktop-mode-my-wordpress-body]";
4165 const STATUS_SEL = "[data-desktop-mode-my-wordpress-status]";
4166 function wpdConfirmGlobal(options) {
4167 const fn = window.wp?.desktop?.confirm;
4168 if (typeof fn !== "function") {
4169 return Promise.resolve(false);
4170 }
4171 return fn(options);
4172 }
4173 function openIframeWindow(opts) {
4174 const manager = window.wp?.desktop?.windowManager;
4175 if (!manager || typeof manager.open !== "function") {
4176 return;
4177 }
4178 manager.open({
4179 id: opts.id,
4180 url: opts.url,
4181 title: opts.title,
4182 icon: opts.icon
4183 });
4184 }
4185 function getThumbnail(item) {
4186 const media = item._embedded?.["wp:featuredmedia"]?.[0];
4187 if (!media) {
4188 return "";
4189 }
4190 const sizes = media.media_details?.sizes;
4191 const preferred = sizes?.medium?.source_url ?? sizes?.thumbnail?.source_url ?? sizes?.large?.source_url ?? media.source_url;
4192 return preferred ?? "";
4193 }
4194 function paintStatus(state, baseSegments, ctx) {
4195 const filtered = applyFilters(
4196 "desktop-mode.my-wordpress.status-bar",
4197 baseSegments,
4198 ctx
4199 );
4200 renderStatusBarSegments(
4201 state.statusBar,
4202 Array.isArray(filtered) ? filtered : baseSegments
4203 );
4204 }
4205 function pluralLabel(n, singular, plural) {
4206 return `${n.toLocaleString()} ${n === 1 ? singular : plural}`;
4207 }
4208 function navigate(state, route, opts = {}) {
4209 const sameRoute = routesEqual(state.route, route);
4210 if (!opts.fromBack && !sameRoute) {
4211 state.history.push(state.route);
4212 }
4213 clearTeardown(state);
4214 state.route = route;
4215 updateBreadcrumbs(state);
4216 state.body.replaceChildren();
4217 if (route.kind === "root") {
4218 renderRoot(state);
4219 return;
4220 }
4221 const entity = getEntity(route.entityId);
4222 if (!entity) {
4223 renderError(
4224 state,
4225 __("Unknown entity type.", "desktop-mode")
4226 );
4227 return;
4228 }
4229 if (route.kind === "list") {
4230 const renderer = getEntityRenderer(entity.kind);
4231 if (renderer) {
4232 const host = makeRenderHost(state);
4233 renderer(host, entity);
4234 return;
4235 }
4236 renderEntityList(state, entity);
4237 return;
4238 }
4239 if (route.kind === "detail") {
4240 renderDetail(state, entity, route.postId, route.postTitle);
4241 return;
4242 }
4243 if (route.kind === "sub-list") {
4244 renderSubList(
4245 state,
4246 entity,
4247 route.postId,
4248 route.postTitle,
4249 route.relation
4250 );
4251 return;
4252 }
4253 if (route.kind === "user-footprint") {
4254 renderUserFootprint(state, entity, route.userId, route.userName);
4255 return;
4256 }
4257 if (route.kind === "media-detail") {
4258 void renderMediaDetail(makeRenderHost(state), route.mediaId);
4259 return;
4260 }
4261 }
4262 function makeRenderHost(state) {
4263 return {
4264 body: state.body,
4265 route: state.route,
4266 navigate: (route) => navigate(state, route),
4267 addTeardown: (fn) => state.teardown.push(fn)
4268 };
4269 }
4270 function routesEqual(a, b) {
4271 if (a.kind !== b.kind) {
4272 return false;
4273 }
4274 switch (a.kind) {
4275 case "root":
4276 return true;
4277 case "list":
4278 return a.entityId === b.entityId;
4279 case "detail": {
4280 const o = b;
4281 return a.entityId === o.entityId && a.postId === o.postId;
4282 }
4283 case "sub-list": {
4284 const o = b;
4285 return a.entityId === o.entityId && a.postId === o.postId && a.relation === o.relation;
4286 }
4287 case "user-footprint": {
4288 const o = b;
4289 return a.entityId === o.entityId && a.userId === o.userId;
4290 }
4291 case "media-detail": {
4292 const o = b;
4293 return a.entityId === o.entityId && a.mediaId === o.mediaId;
4294 }
4295 default:
4296 return false;
4297 }
4298 }
4299 function parentRoute(route) {
4300 switch (route.kind) {
4301 case "root":
4302 return route;
4303 case "list":
4304 return { kind: "root" };
4305 case "detail":
4306 return { kind: "list", entityId: route.entityId };
4307 case "sub-list":
4308 return {
4309 kind: "detail",
4310 entityId: route.entityId,
4311 postId: route.postId,
4312 postTitle: route.postTitle
4313 };
4314 case "user-footprint":
4315 return { kind: "list", entityId: route.entityId };
4316 case "media-detail":
4317 return { kind: "list", entityId: route.entityId };
4318 default:
4319 return { kind: "root" };
4320 }
4321 }
4322 function clearTeardown(state) {
4323 for (const fn of state.teardown) {
4324 try {
4325 fn();
4326 } catch {
4327 }
4328 }
4329 state.teardown = [];
4330 }
4331 function updateBreadcrumbs(state) {
4332 const { route } = state;
4333 const segments = [];
4334 const isRoot = route.kind === "root";
4335 segments.push(
4336 isRoot ? { label: __("My WordPress", "desktop-mode") } : {
4337 label: __("My WordPress", "desktop-mode"),
4338 onClick: () => navigate(state, { kind: "root" })
4339 }
4340 );
4341 if (route.kind !== "root") {
4342 const entity = getEntity(route.entityId);
4343 const label = entity ? entity.label : route.entityId;
4344 segments.push(
4345 route.kind === "list" ? { label } : {
4346 label,
4347 onClick: () => navigate(state, {
4348 kind: "list",
4349 entityId: route.entityId
4350 })
4351 }
4352 );
4353 }
4354 if (route.kind === "detail" || route.kind === "sub-list") {
4355 const postTitle = route.postTitle;
4356 const entityId = route.entityId;
4357 const postId = route.postId;
4358 segments.push(
4359 route.kind === "detail" ? { label: postTitle } : {
4360 label: postTitle,
4361 onClick: () => navigate(state, {
4362 kind: "detail",
4363 entityId,
4364 postId,
4365 postTitle
4366 })
4367 }
4368 );
4369 }
4370 if (route.kind === "sub-list") {
4371 segments.push({ label: subRelationLabel(route.relation) });
4372 }
4373 if (route.kind === "user-footprint") {
4374 segments.push({
4375 label: sprintf(
4376 // translators: %s is a user display name.
4377 __("%s — activity footprint", "desktop-mode"),
4378 route.userName
4379 )
4380 });
4381 }
4382 if (route.kind === "media-detail") {
4383 segments.push({ label: route.mediaTitle });
4384 }
4385 renderBreadcrumbs(state.breadcrumbs, segments, {
4386 onBack: () => {
4387 const previous = state.history.pop();
4388 if (previous) {
4389 navigate(state, previous, { fromBack: true });
4390 return;
4391 }
4392 navigate(state, parentRoute(state.route), { fromBack: true });
4393 },
4394 backDisabled: isRoot && state.history.length === 0
4395 });
4396 }
4397 function subRelationLabel(relation) {
4398 switch (relation) {
4399 case "author":
4400 return __("Author", "desktop-mode");
4401 case "contributors":
4402 return __("Contributors", "desktop-mode");
4403 case "comments":
4404 return __("Comments", "desktop-mode");
4405 case "categories":
4406 return __("Categories", "desktop-mode");
4407 case "tags":
4408 return __("Tags", "desktop-mode");
4409 case "media":
4410 return __("Attached media", "desktop-mode");
4411 case "revisions":
4412 return __("Revisions", "desktop-mode");
4413 default:
4414 return relation;
4415 }
4416 }
4417 function renderRoot(state) {
4418 const cfg = getConfig();
4419 const grid = document.createElement("div");
4420 grid.className = "desktop-mode-my-wordpress__grid desktop-mode-my-wordpress__canvas";
4421 grid.setAttribute("role", "list");
4422 const layout = createTileLayout(grid, "root");
4423 const select = createTileSelector();
4424 const tilesByEntity = /* @__PURE__ */ new Map();
4425 cfg.entities.forEach((entity, idx) => {
4426 const tile = buildIconTile({
4427 role: "folder",
4428 icon: entity.icon,
4429 label: entity.label
4430 });
4431 tile.dataset.entityId = entity.id;
4432 tilesByEntity.set(entity.id, tile);
4433 const tileKey = `entity:${entity.id}`;
4434 const synthDate = new Date(2020, 0, 1 + idx).toISOString();
4435 layout.place(tile, tileKey, {
4436 name: entity.label,
4437 date: synthDate
4438 });
4439 tile.addEventListener("click", () => select(tile));
4440 tile.addEventListener("dblclick", (e) => {
4441 e.preventDefault();
4442 navigate(state, { kind: "list", entityId: entity.id });
4443 });
4444 grid.appendChild(tile);
4445 });
4446 cfg.entities.forEach((entity) => {
4447 void fetchEntityTotal(entity).then((total) => {
4448 if (state.route.kind !== "root") {
4449 return;
4450 }
4451 const tile = tilesByEntity.get(entity.id);
4452 if (!tile) {
4453 return;
4454 }
4455 const label = tile.querySelector(
4456 ".desktop-mode-file-tile__label"
4457 );
4458 if (label) {
4459 label.textContent = `${entity.label} · ${total.toLocaleString()}`;
4460 }
4461 }).catch(() => {
4462 });
4463 });
4464 state.body.appendChild(grid);
4465 const menu = attachIconCanvasMenu(grid, {
4466 scope: "my-wordpress:root",
4467 onSort: (mode) => layout.sort(mode)
4468 });
4469 state.teardown.push(() => menu.dispose());
4470 state.teardown.push(() => layout.dispose());
4471 paintStatus(
4472 state,
4473 [
4474 {
4475 id: "count",
4476 label: pluralLabel(cfg.entities.length, "folder", "folders"),
4477 align: "start",
4478 sort: 10
4479 }
4480 ],
4481 { view: "root" }
4482 );
4483 }
4484 function buildIconTile(spec) {
4485 return buildTileFromSpec({
4486 type: spec.role === "folder" ? "folder" : "__my-wordpress-entry",
4487 ref: spec.label,
4488 label: spec.label,
4489 icon: sanitizeClass(spec.icon),
4490 role: spec.role,
4491 extraClasses: [
4492 "desktop-mode-my-wordpress__tile",
4493 spec.role === "folder" ? "desktop-mode-my-wordpress__tile--folder" : "desktop-mode-my-wordpress__tile--entry"
4494 ]
4495 });
4496 }
4497 function renderError(state, message) {
4498 const empty = document.createElement("div");
4499 empty.className = "desktop-mode-my-wordpress__empty";
4500 empty.textContent = message;
4501 state.body.appendChild(empty);
4502 }
4503 const lastQueryByEntity = /* @__PURE__ */ new Map();
4504 function renderEntityList(state, entity) {
4505 const cfg = getConfig();
4506 const initialQuery = lastQueryByEntity.get(entity.id) ?? "";
4507 const toolbar = renderListToolbar({
4508 placeholder: sprintf(
4509 // translators: %s is a lowercased entity-type label (e.g. "posts", "pages").
4510 __("Search %s…", "desktop-mode"),
4511 entity.label.toLowerCase()
4512 ),
4513 ariaLabel: sprintf(
4514 // translators: %s is an entity-type label (e.g. "Posts", "Pages").
4515 __("Search %s", "desktop-mode"),
4516 entity.label
4517 ),
4518 initialValue: initialQuery,
4519 onSearchChange: (q) => {
4520 lastQueryByEntity.set(entity.id, q);
4521 void resetForSearch(q);
4522 }
4523 });
4524 state.body.appendChild(toolbar.host);
4525 state.teardown.push(() => toolbar.destroy());
4526 const split = document.createElement("div");
4527 split.className = "desktop-mode-my-wordpress__split";
4528 const left = document.createElement("div");
4529 left.className = "desktop-mode-my-wordpress__list";
4530 const tiles = document.createElement("div");
4531 tiles.className = "desktop-mode-my-wordpress__tiles desktop-mode-my-wordpress__canvas";
4532 tiles.setAttribute("role", "list");
4533 left.appendChild(tiles);
4534 const sentinel = document.createElement("div");
4535 sentinel.className = "desktop-mode-my-wordpress__sentinel";
4536 sentinel.setAttribute("aria-hidden", "true");
4537 left.appendChild(sentinel);
4538 const right = document.createElement("div");
4539 right.className = "desktop-mode-my-wordpress__preview";
4540 const previewEmpty = document.createElement("div");
4541 previewEmpty.className = "desktop-mode-my-wordpress__preview-empty";
4542 previewEmpty.textContent = __(
4543 "Select an entry to preview it here.",
4544 "desktop-mode"
4545 );
4546 right.appendChild(previewEmpty);
4547 split.appendChild(left);
4548 split.appendChild(right);
4549 state.body.appendChild(split);
4550 const tileLayout = createTileLayout(tiles, `entity:${entity.id}`);
4551 const menu = attachIconCanvasMenu(tiles, {
4552 scope: `my-wordpress:${entity.id}`,
4553 onSort: (mode) => tileLayout.sort(mode)
4554 });
4555 state.teardown.push(() => menu.dispose());
4556 const ctx = {
4557 page: 0,
4558 totalPages: 1,
4559 total: 0,
4560 loaded: 0,
4561 loading: false,
4562 done: false,
4563 tiles,
4564 sentinel,
4565 preview: right,
4566 selectedId: null,
4567 selectedTile: null,
4568 observer: null,
4569 layout: tileLayout,
4570 query: initialQuery,
4571 abort: null
4572 };
4573 state.teardown.push(() => tileLayout.dispose());
4574 state.teardown.push(() => ctx.abort?.abort());
4575 const repaintListStatus = () => {
4576 let itemLabel;
4577 if (ctx.total === 0 && ctx.loaded === 0) {
4578 itemLabel = pluralLabel(0, "item", "items");
4579 } else if (ctx.total > ctx.loaded && ctx.loaded > 0) {
4580 itemLabel = sprintf(
4581 // translators: 1: visible item count, 2: total item count.
4582 __("%1$d of %2$d items", "desktop-mode"),
4583 ctx.loaded,
4584 ctx.total
4585 );
4586 } else {
4587 itemLabel = pluralLabel(
4588 Math.max(ctx.total, ctx.loaded),
4589 "item",
4590 "items"
4591 );
4592 }
4593 const segments = [
4594 { id: "count", label: itemLabel, align: "start", sort: 10 }
4595 ];
4596 if (ctx.totalPages > 1) {
4597 segments.push({
4598 id: "page",
4599 label: sprintf(
4600 // translators: 1: current page, 2: total pages.
4601 __("Page %1$d of %2$d", "desktop-mode"),
4602 Math.max(ctx.page, 1),
4603 ctx.totalPages
4604 ),
4605 align: "end",
4606 sort: 10
4607 });
4608 }
4609 paintStatus(state, segments, {
4610 view: "list",
4611 entityId: entity.id
4612 });
4613 };
4614 repaintListStatus();
4615 const sentinelIsVisible = () => {
4616 const sr = sentinel.getBoundingClientRect();
4617 const rr = left.getBoundingClientRect();
4618 const slack = 200;
4619 return sr.top < rr.bottom + slack && sr.bottom > rr.top - slack;
4620 };
4621 const loadMore = async () => {
4622 if (ctx.loading || ctx.done) {
4623 return;
4624 }
4625 ctx.loading = true;
4626 const nextPage = ctx.page + 1;
4627 const isFirst = nextPage === 1;
4628 const queryAtFetchTime = ctx.query;
4629 showLoadingSkeleton(tiles, ctx.layout, isFirst);
4630 const controller = new AbortController();
4631 ctx.abort = controller;
4632 try {
4633 const result = await fetchEntityList(entity, {
4634 page: nextPage,
4635 perPage: cfg.perPage,
4636 search: queryAtFetchTime || void 0,
4637 signal: controller.signal
4638 });
4639 if (ctx.query !== queryAtFetchTime) {
4640 return;
4641 }
4642 ctx.page = nextPage;
4643 ctx.totalPages = result.totalPages;
4644 ctx.total = result.total;
4645 hideLoadingSkeleton(tiles);
4646 if (result.items.length === 0 && isFirst) {
4647 renderListEmpty(tiles, entity, queryAtFetchTime);
4648 ctx.done = true;
4649 repaintListStatus();
4650 return;
4651 }
4652 for (const item of result.items) {
4653 tiles.appendChild(buildEntityTile(state, ctx, entity, item));
4654 ctx.loaded += 1;
4655 }
4656 if (ctx.page >= ctx.totalPages) {
4657 ctx.done = true;
4658 }
4659 repaintListStatus();
4660 } catch (err) {
4661 if (isAbortError(err)) {
4662 return;
4663 }
4664 hideLoadingSkeleton(tiles);
4665 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
4666 renderListError(tiles, msg);
4667 ctx.done = true;
4668 } finally {
4669 ctx.loading = false;
4670 if (ctx.abort === controller) {
4671 ctx.abort = null;
4672 }
4673 }
4674 if (!ctx.done) {
4675 requestAnimationFrame(() => {
4676 if (sentinelIsVisible()) {
4677 void loadMore();
4678 }
4679 });
4680 }
4681 };
4682 const resetForSearch = async (q) => {
4683 ctx.abort?.abort();
4684 ctx.abort = null;
4685 ctx.query = q;
4686 tiles.classList.add(
4687 "desktop-mode-my-wordpress__tiles--searching"
4688 );
4689 hideLoadingSkeleton(tiles);
4690 const controller = new AbortController();
4691 ctx.abort = controller;
4692 ctx.loading = true;
4693 try {
4694 const result = await fetchEntityList(entity, {
4695 page: 1,
4696 perPage: cfg.perPage,
4697 search: q || void 0,
4698 signal: controller.signal
4699 });
4700 if (ctx.query !== q) {
4701 return;
4702 }
4703 tiles.replaceChildren();
4704 ctx.layout.clear();
4705 tiles.classList.remove(
4706 "desktop-mode-my-wordpress__tiles--searching"
4707 );
4708 ctx.page = 1;
4709 ctx.totalPages = result.totalPages;
4710 ctx.total = result.total;
4711 ctx.loaded = 0;
4712 ctx.done = ctx.page >= ctx.totalPages;
4713 ctx.selectedId = null;
4714 ctx.selectedTile = null;
4715 ctx.preview.replaceChildren();
4716 const emptyPreview = document.createElement("div");
4717 emptyPreview.className = "desktop-mode-my-wordpress__preview-empty";
4718 emptyPreview.textContent = __(
4719 "Select an entry to preview it here.",
4720 "desktop-mode"
4721 );
4722 ctx.preview.appendChild(emptyPreview);
4723 if (result.items.length === 0) {
4724 renderListEmpty(tiles, entity, q);
4725 ctx.done = true;
4726 } else {
4727 for (const item of result.items) {
4728 tiles.appendChild(
4729 buildEntityTile(state, ctx, entity, item)
4730 );
4731 ctx.loaded += 1;
4732 }
4733 }
4734 repaintListStatus();
4735 } catch (err) {
4736 if (isAbortError(err)) {
4737 return;
4738 }
4739 tiles.classList.remove(
4740 "desktop-mode-my-wordpress__tiles--searching"
4741 );
4742 tiles.replaceChildren();
4743 ctx.layout.clear();
4744 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
4745 renderListError(tiles, msg);
4746 ctx.done = true;
4747 } finally {
4748 ctx.loading = false;
4749 if (ctx.abort === controller) {
4750 ctx.abort = null;
4751 }
4752 }
4753 if (!ctx.done) {
4754 requestAnimationFrame(() => {
4755 if (sentinelIsVisible()) {
4756 void loadMore();
4757 }
4758 });
4759 }
4760 };
4761 if (typeof IntersectionObserver !== "undefined") {
4762 ctx.observer = new IntersectionObserver(
4763 (entries) => {
4764 for (const e of entries) {
4765 if (e.isIntersecting) {
4766 void loadMore();
4767 }
4768 }
4769 },
4770 { root: left, rootMargin: "200px 0px" }
4771 );
4772 ctx.observer.observe(sentinel);
4773 state.teardown.push(() => ctx.observer?.disconnect());
4774 }
4775 void loadMore();
4776 }
4777 function isAbortError(err) {
4778 return err instanceof DOMException && err.name === "AbortError";
4779 }
4780 function renderListEmpty(host, entity, query) {
4781 const empty = document.createElement("div");
4782 empty.className = "desktop-mode-my-wordpress__empty";
4783 if (query) {
4784 empty.textContent = sprintf(
4785 // translators: 1: search query, 2: lowercased entity-type label.
4786 __('No %2$s match "%1$s".', "desktop-mode"),
4787 query,
4788 entity.label.toLowerCase()
4789 );
4790 } else {
4791 empty.textContent = sprintf(
4792 // translators: %s is an entity-type label (e.g. "Posts", "Pages").
4793 __("No %s yet.", "desktop-mode"),
4794 entity.label.toLowerCase()
4795 );
4796 }
4797 host.appendChild(empty);
4798 }
4799 function renderListError(host, message) {
4800 const err = document.createElement("div");
4801 err.className = "desktop-mode-my-wordpress__error";
4802 err.textContent = message;
4803 host.appendChild(err);
4804 }
4805 function buildSkeletonTile(variant) {
4806 const tile = document.createElement("div");
4807 tile.className = "desktop-mode-my-wordpress__skeleton-tile";
4808 tile.dataset.loadingSkeleton = variant;
4809 tile.setAttribute("aria-hidden", "true");
4810 const icon = document.createElement("div");
4811 icon.className = "desktop-mode-my-wordpress__skeleton-icon";
4812 tile.appendChild(icon);
4813 const label = document.createElement("div");
4814 label.className = "desktop-mode-my-wordpress__skeleton-label";
4815 tile.appendChild(label);
4816 return tile;
4817 }
4818 const SKELETON_LABEL_WIDTHS = [72, 60, 82, 48, 70];
4819 const SKELETON_DELAY_STEPS = [0, 0.18, 0.36, 0.54, 0.12];
4820 function showLoadingSkeleton(host, layout, isFirst) {
4821 const variant = isFirst ? "first" : "more";
4822 if (host.querySelector(`[data-loading-skeleton="${variant}"]`)) {
4823 return;
4824 }
4825 const count = isFirst ? 8 : 4;
4826 const cells = layout.peekNextCells(count);
4827 let maxBottom = parseFloat(host.style.minHeight || "0");
4828 cells.forEach((cell, i) => {
4829 const tile = buildSkeletonTile(variant);
4830 tile.style.left = `${cell.x}px`;
4831 tile.style.top = `${cell.y}px`;
4832 tile.style.setProperty(
4833 "--desktop-mode-skeleton-delay",
4834 `${SKELETON_DELAY_STEPS[i % SKELETON_DELAY_STEPS.length]}s`
4835 );
4836 const label = tile.querySelector(
4837 ".desktop-mode-my-wordpress__skeleton-label"
4838 );
4839 if (label) {
4840 label.style.width = `${SKELETON_LABEL_WIDTHS[i % SKELETON_LABEL_WIDTHS.length]}%`;
4841 }
4842 host.appendChild(tile);
4843 maxBottom = Math.max(maxBottom, cell.y + TILE_H);
4844 });
4845 host.style.minHeight = `${maxBottom + TILE_PAD}px`;
4846 }
4847 function hideLoadingSkeleton(host) {
4848 host.querySelectorAll("[data-loading-skeleton]").forEach(
4849 (n) => n.remove()
4850 );
4851 }
4852 function buildEntityTile(state, ctx, entity, item) {
4853 const titleText = stripTags(item.title.rendered) || __("(no title)", "desktop-mode");
4854 const tile = buildIconTile({
4855 role: "entry",
4856 icon: entity.icon,
4857 label: titleText
4858 });
4859 tile.dataset.entryId = String(item.id);
4860 if (item.status) {
4861 tile.setAttribute("status", item.status);
4862 }
4863 attachTileDragOut(
4864 tile,
4865 {
4866 kind: "post",
4867 ref: String(item.id),
4868 title: titleText,
4869 icon: entity.icon,
4870 // Source entity id (`'posts'` / `'pages'` / future
4871 // CPT-backed entities). Lets the recycle bin's drop
4872 // handler resolve the right REST endpoint when the user
4873 // drags this tile to the bin to trash it.
4874 entityId: entity.id,
4875 // Cross-frame bridge payload — the Gutenberg drop-receiver
4876 // turns this into a `core/paragraph` with an `<a href>` to
4877 // the permalink. Tiles without a `link` (very old REST
4878 // shapes / private posts) still drag-out for placement
4879 // purposes; the receiver no-ops on an empty url.
4880 bridgePayload: {
4881 kind: "post",
4882 id: item.id,
4883 postType: entity.id,
4884 url: item.link ?? "",
4885 title: titleText
4886 }
4887 },
4888 () => hideTooltip()
4889 );
4890 const lock = item.desktop_mode_lock ?? null;
4891 if (lock) {
4892 tile.classList.add("desktop-mode-my-wordpress__tile--locked");
4893 const badge = document.createElement("span");
4894 badge.className = "desktop-mode-my-wordpress__tile-lock dashicons dashicons-lock";
4895 badge.setAttribute("aria-hidden", "true");
4896 tile.appendChild(badge);
4897 const lockedAriaLabel = __(
4898 "%1$s — currently being edited by %2$s",
4899 "desktop-mode"
4900 );
4901 tile.setAttribute(
4902 "aria-label",
4903 sprintf(lockedAriaLabel, titleText, lock.userName)
4904 );
4905 }
4906 let tooltip = null;
4907 const showTooltip = (ev) => {
4908 if (!tooltip) {
4909 tooltip = buildTooltip(titleText, item);
4910 }
4911 document.body.appendChild(tooltip);
4912 positionTooltip(tooltip, ev);
4913 };
4914 const moveTooltip = (ev) => {
4915 if (tooltip && tooltip.isConnected) {
4916 positionTooltip(tooltip, ev);
4917 }
4918 };
4919 const hideTooltip = () => {
4920 if (tooltip && tooltip.isConnected) {
4921 tooltip.remove();
4922 }
4923 };
4924 tile.addEventListener("mouseenter", showTooltip);
4925 tile.addEventListener("mousemove", moveTooltip);
4926 tile.addEventListener("mouseleave", hideTooltip);
4927 state.teardown.push(hideTooltip);
4928 const tileKey = `entry:${item.id}`;
4929 ctx.layout.place(tile, tileKey, {
4930 name: titleText,
4931 date: item.date || (/* @__PURE__ */ new Date(0)).toISOString()
4932 });
4933 tile.addEventListener("click", () => {
4934 selectTile(state, ctx, tile, entity, item.id);
4935 });
4936 tile.addEventListener("dblclick", (e) => {
4937 e.preventDefault();
4938 hideTooltip();
4939 openEditor(entity, item.id, titleText);
4940 });
4941 tile.addEventListener("contextmenu", (e) => {
4942 e.preventDefault();
4943 hideTooltip();
4944 openTileMenu(state, ctx, entity, item, titleText, {
4945 x: e.clientX,
4946 y: e.clientY
4947 });
4948 });
4949 return tile;
4950 }
4951 function buildTooltip(title, item) {
4952 const tip = document.createElement("div");
4953 tip.className = "desktop-mode-my-wordpress__tooltip";
4954 tip.setAttribute("role", "tooltip");
4955 const heading = document.createElement("div");
4956 heading.className = "desktop-mode-my-wordpress__tooltip-title";
4957 heading.textContent = title;
4958 tip.appendChild(heading);
4959 const lock = item.desktop_mode_lock ?? null;
4960 if (lock) {
4961 const banner = document.createElement("div");
4962 banner.className = "desktop-mode-my-wordpress__tooltip-lock";
4963 const icon = document.createElement("span");
4964 icon.className = "dashicons dashicons-lock";
4965 icon.setAttribute("aria-hidden", "true");
4966 banner.appendChild(icon);
4967 const text = document.createElement("span");
4968 text.textContent = sprintf(
4969 // translators: %s is the user name currently editing the post.
4970 __("%s is currently editing", "desktop-mode"),
4971 lock.userName
4972 );
4973 banner.appendChild(text);
4974 tip.appendChild(banner);
4975 }
4976 const thumb = getThumbnail(item);
4977 if (thumb) {
4978 const img = document.createElement("img");
4979 img.className = "desktop-mode-my-wordpress__tooltip-thumb";
4980 img.src = thumb;
4981 img.alt = "";
4982 tip.appendChild(img);
4983 }
4984 const excerpt = stripTags(item.excerpt?.rendered ?? "");
4985 if (excerpt) {
4986 const p = document.createElement("p");
4987 p.className = "desktop-mode-my-wordpress__tooltip-excerpt";
4988 p.textContent = excerpt.length > 240 ? excerpt.slice(0, 237) + "" : excerpt;
4989 tip.appendChild(p);
4990 }
4991 return tip;
4992 }
4993 function positionTooltip(tip, ev) {
4994 const offset = 16;
4995 let x = ev.clientX + offset;
4996 let y = ev.clientY + offset;
4997 const rect = tip.getBoundingClientRect();
4998 if (x + rect.width > window.innerWidth - 8) {
4999 x = Math.max(8, ev.clientX - rect.width - offset);
5000 }
5001 if (y + rect.height > window.innerHeight - 8) {
5002 y = Math.max(8, ev.clientY - rect.height - offset);
5003 }
5004 tip.style.left = `${x}px`;
5005 tip.style.top = `${y}px`;
5006 }
5007 function selectTile(state, ctx, tile, entity, id) {
5008 if (ctx.selectedTile) {
5009 ctx.selectedTile.classList.remove(
5010 "desktop-mode-file-tile--selected"
5011 );
5012 }
5013 tile.classList.add("desktop-mode-file-tile--selected");
5014 ctx.selectedTile = tile;
5015 ctx.selectedId = id;
5016 void renderPreview(state, ctx, entity, id);
5017 }
5018 async function renderPreview(state, ctx, entity, id) {
5019 showPreviewLoading(ctx.preview);
5020 let detail;
5021 try {
5022 detail = await fetchEntityDetail(entity, id);
5023 } catch (err) {
5024 ctx.preview.replaceChildren();
5025 if (ctx.selectedId !== id) {
5026 return;
5027 }
5028 showPreviewError(ctx.preview, err);
5029 return;
5030 }
5031 if (ctx.selectedId !== id) {
5032 return;
5033 }
5034 appendPostArticle(ctx.preview, detail, entity, {
5035 onExplore: () => {
5036 navigate(state, {
5037 kind: "detail",
5038 entityId: entity.id,
5039 postId: detail.id,
5040 postTitle: stripTags(detail.title.rendered)
5041 });
5042 }
5043 });
5044 }
5045 function showPreviewLoading(host) {
5046 host.replaceChildren();
5047 const loading = document.createElement("div");
5048 loading.className = "desktop-mode-my-wordpress__preview-loading";
5049 const spinner = document.createElement("wpd-spinner");
5050 loading.appendChild(spinner);
5051 host.appendChild(loading);
5052 }
5053 function showPreviewError(host, err) {
5054 host.replaceChildren();
5055 const box = document.createElement("div");
5056 box.className = "desktop-mode-my-wordpress__error";
5057 box.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
5058 host.appendChild(box);
5059 }
5060 function appendPostArticle(host, detail, entity, opts = {}) {
5061 host.replaceChildren();
5062 const article = document.createElement("article");
5063 article.className = "desktop-mode-my-wordpress__article";
5064 const heading = document.createElement("h2");
5065 heading.className = "desktop-mode-my-wordpress__article-title";
5066 heading.textContent = stripTags(detail.title.rendered);
5067 article.appendChild(heading);
5068 const meta = buildPostMetaLine(detail);
5069 if (meta) {
5070 article.appendChild(meta);
5071 }
5072 const thumb = getThumbnail(detail);
5073 if (thumb) {
5074 const img = document.createElement("img");
5075 img.className = "desktop-mode-my-wordpress__article-hero";
5076 img.src = thumb;
5077 img.alt = "";
5078 article.appendChild(img);
5079 }
5080 const content = document.createElement("div");
5081 content.className = "desktop-mode-my-wordpress__article-content";
5082 content.innerHTML = detail.content.rendered;
5083 article.appendChild(content);
5084 const footer = document.createElement("footer");
5085 footer.className = "desktop-mode-my-wordpress__article-footer";
5086 if (opts.onExplore) {
5087 const exploreBtn = document.createElement("wpd-button");
5088 exploreBtn.setAttribute("variant", "secondary");
5089 exploreBtn.textContent = __("Explore details", "desktop-mode");
5090 exploreBtn.title = __(
5091 "See author, comments, categories, tags, attached media, and revisions for this entry.",
5092 "desktop-mode"
5093 );
5094 exploreBtn.addEventListener("click", () => {
5095 opts.onExplore?.();
5096 });
5097 footer.appendChild(exploreBtn);
5098 }
5099 const editBtn = document.createElement("wpd-button");
5100 editBtn.setAttribute("variant", "primary");
5101 editBtn.textContent = __("Open in editor", "desktop-mode");
5102 editBtn.addEventListener("click", () => {
5103 openEditor(entity, detail.id, stripTags(detail.title.rendered));
5104 });
5105 footer.appendChild(editBtn);
5106 article.appendChild(footer);
5107 host.appendChild(article);
5108 }
5109 function buildPostMetaLine(detail) {
5110 const parts = [];
5111 const author = detail._embedded?.author?.[0];
5112 if (author?.name) {
5113 parts.push(author.name);
5114 }
5115 if (detail.date) {
5116 try {
5117 parts.push(
5118 new Date(detail.date).toLocaleDateString(void 0, {
5119 year: "numeric",
5120 month: "long",
5121 day: "numeric"
5122 })
5123 );
5124 } catch {
5125 parts.push(detail.date);
5126 }
5127 }
5128 if (detail.status && detail.status !== "publish") {
5129 parts.push(detail.status);
5130 }
5131 if (parts.length === 0) {
5132 return null;
5133 }
5134 const line = document.createElement("p");
5135 line.className = "desktop-mode-my-wordpress__article-meta";
5136 line.textContent = parts.join(" · ");
5137 return line;
5138 }
5139 function renderDetail(state, entity, postId, postTitle) {
5140 const split = document.createElement("div");
5141 split.className = "desktop-mode-my-wordpress__split";
5142 const left = document.createElement("div");
5143 left.className = "desktop-mode-my-wordpress__list";
5144 const tiles = document.createElement("div");
5145 tiles.className = "desktop-mode-my-wordpress__tiles desktop-mode-my-wordpress__canvas";
5146 tiles.setAttribute("role", "list");
5147 left.appendChild(tiles);
5148 const right = document.createElement("div");
5149 right.className = "desktop-mode-my-wordpress__preview";
5150 showPreviewLoading(right);
5151 split.appendChild(left);
5152 split.appendChild(right);
5153 state.body.appendChild(split);
5154 const layout = createTileLayout(
5155 tiles,
5156 `detail:${entity.id}:${postId}`
5157 );
5158 const menu = attachIconCanvasMenu(tiles, {
5159 scope: `my-wordpress:${entity.id}:detail:${postId}`,
5160 onSort: (mode) => layout.sort(mode)
5161 });
5162 state.teardown.push(() => menu.dispose());
5163 state.teardown.push(() => layout.dispose());
5164 showLoadingSkeleton(tiles, layout, true);
5165 void (async () => {
5166 let detail;
5167 try {
5168 detail = await fetchEntityDetail(entity, postId);
5169 } catch (err) {
5170 hideLoadingSkeleton(tiles);
5171 renderListError(
5172 tiles,
5173 err instanceof Error ? err.message : __("Unknown error.", "desktop-mode")
5174 );
5175 showPreviewError(right, err);
5176 return;
5177 }
5178 if (state.route.kind !== "detail" || state.route.postId !== postId) {
5179 return;
5180 }
5181 hideLoadingSkeleton(tiles);
5182 const select = createTileSelector();
5183 const subFolders = [];
5184 let dateCounter = 0;
5185 const nextDate = () => new Date(2020, 0, 1 + dateCounter++).toISOString();
5186 const author = detail._embedded?.author?.[0];
5187 subFolders.push({
5188 relation: "author",
5189 label: author?.name ? sprintf(
5190 // translators: %s is an author display name.
5191 __("Author · %s", "desktop-mode"),
5192 author.name
5193 ) : __("Author", "desktop-mode"),
5194 icon: "dashicons-admin-users",
5195 count: 1,
5196 disabled: !detail.author,
5197 synthDate: nextDate()
5198 });
5199 const contributors = detail.desktop_mode_contributors ?? [];
5200 if (contributors.length > 0) {
5201 subFolders.push({
5202 relation: "contributors",
5203 label: sprintf(
5204 // translators: %d is a count of additional contributor users.
5205 _n(
5206 "Contributors · %d",
5207 "Contributors · %d",
5208 contributors.length
5209 ),
5210 contributors.length
5211 ),
5212 icon: "dashicons-groups",
5213 count: contributors.length,
5214 synthDate: nextDate()
5215 });
5216 }
5217 const commentsHref = (detail._links?.replies ?? [])[0];
5218 const commentCountFromLink = typeof commentsHref?.count === "number" ? commentsHref.count : null;
5219 const repliesEmbed = detail._embedded?.replies?.[0] ?? [];
5220 const commentCount = commentCountFromLink ?? repliesEmbed.length;
5221 subFolders.push({
5222 relation: "comments",
5223 label: sprintf(
5224 // translators: %d is a comment count.
5225 _n("Comments · %d", "Comments · %d", commentCount),
5226 commentCount
5227 ),
5228 icon: "dashicons-admin-comments",
5229 count: commentCount,
5230 disabled: detail.comment_status === "closed" && commentCount === 0,
5231 synthDate: nextDate()
5232 });
5233 const categoryIds = detail.categories ?? [];
5234 if (categoryIds.length > 0) {
5235 subFolders.push({
5236 relation: "categories",
5237 label: sprintf(
5238 // translators: %d is a category count.
5239 _n("Categories · %d", "Categories · %d", categoryIds.length),
5240 categoryIds.length
5241 ),
5242 icon: "dashicons-category",
5243 count: categoryIds.length,
5244 synthDate: nextDate()
5245 });
5246 }
5247 const tagIds = detail.tags ?? [];
5248 if (tagIds.length > 0) {
5249 subFolders.push({
5250 relation: "tags",
5251 label: sprintf(
5252 // translators: %d is a tag count.
5253 _n("Tags · %d", "Tags · %d", tagIds.length),
5254 tagIds.length
5255 ),
5256 icon: "dashicons-tag",
5257 count: tagIds.length,
5258 synthDate: nextDate()
5259 });
5260 }
5261 if (detail.featured_media && detail.featured_media > 0) {
5262 subFolders.push({
5263 relation: "media",
5264 label: __("Attached media", "desktop-mode"),
5265 icon: "dashicons-format-image",
5266 count: 1,
5267 synthDate: nextDate()
5268 });
5269 } else {
5270 subFolders.push({
5271 relation: "media",
5272 label: __("Attached media", "desktop-mode"),
5273 icon: "dashicons-admin-media",
5274 count: 0,
5275 synthDate: nextDate()
5276 });
5277 }
5278 subFolders.push({
5279 relation: "revisions",
5280 label: __("Revisions", "desktop-mode"),
5281 icon: "dashicons-backup",
5282 count: 0,
5283 synthDate: nextDate()
5284 });
5285 for (const sub of subFolders) {
5286 const tile = buildIconTile({
5287 role: "folder",
5288 icon: sub.icon,
5289 label: sub.label
5290 });
5291 tile.dataset.relation = sub.relation;
5292 if (sub.disabled) {
5293 tile.setAttribute("aria-disabled", "true");
5294 }
5295 const tileKey = `relation:${sub.relation}`;
5296 layout.place(tile, tileKey, {
5297 name: sub.label,
5298 date: sub.synthDate
5299 });
5300 tile.addEventListener("click", () => select(tile));
5301 if (!sub.disabled) {
5302 tile.addEventListener("dblclick", (e) => {
5303 e.preventDefault();
5304 navigate(state, {
5305 kind: "sub-list",
5306 entityId: entity.id,
5307 postId,
5308 postTitle,
5309 relation: sub.relation
5310 });
5311 });
5312 }
5313 tiles.appendChild(tile);
5314 }
5315 appendPostArticle(right, detail, entity);
5316 const segments = [
5317 {
5318 id: "count",
5319 label: pluralLabel(
5320 subFolders.length,
5321 "folder",
5322 "folders"
5323 ),
5324 align: "start",
5325 sort: 10
5326 }
5327 ];
5328 if (detail.status) {
5329 segments.push({
5330 id: "status",
5331 label: detail.status,
5332 align: "end",
5333 sort: 10
5334 });
5335 }
5336 paintStatus(state, segments, {
5337 view: "detail",
5338 entityId: entity.id,
5339 postId
5340 });
5341 })();
5342 paintStatus(
5343 state,
5344 [
5345 {
5346 id: "loading",
5347 label: __("Loading…", "desktop-mode"),
5348 align: "start",
5349 sort: 10
5350 }
5351 ],
5352 { view: "detail", entityId: entity.id, postId }
5353 );
5354 }
5355 function renderSubList(state, entity, postId, postTitle, relation) {
5356 const split = document.createElement("div");
5357 split.className = "desktop-mode-my-wordpress__split";
5358 const left = document.createElement("div");
5359 left.className = "desktop-mode-my-wordpress__list";
5360 const tiles = document.createElement("div");
5361 tiles.className = "desktop-mode-my-wordpress__tiles desktop-mode-my-wordpress__canvas";
5362 tiles.setAttribute("role", "list");
5363 left.appendChild(tiles);
5364 const right = document.createElement("div");
5365 right.className = "desktop-mode-my-wordpress__preview";
5366 const previewEmpty = document.createElement("div");
5367 previewEmpty.className = "desktop-mode-my-wordpress__preview-empty";
5368 previewEmpty.textContent = __(
5369 "Select an item to preview it here.",
5370 "desktop-mode"
5371 );
5372 right.appendChild(previewEmpty);
5373 split.appendChild(left);
5374 split.appendChild(right);
5375 state.body.appendChild(split);
5376 const layout = createTileLayout(
5377 tiles,
5378 `sub-list:${entity.id}:${postId}:${relation}`
5379 );
5380 const menu = attachIconCanvasMenu(tiles, {
5381 scope: `my-wordpress:${entity.id}:${relation}:${postId}`,
5382 onSort: (mode) => layout.sort(mode)
5383 });
5384 state.teardown.push(() => menu.dispose());
5385 state.teardown.push(() => layout.dispose());
5386 showLoadingSkeleton(tiles, layout, true);
5387 paintStatus(
5388 state,
5389 [
5390 {
5391 id: "loading",
5392 label: __("Loading…", "desktop-mode"),
5393 align: "start",
5394 sort: 10
5395 }
5396 ],
5397 { view: "sub-list", entityId: entity.id, postId, relation }
5398 );
5399 void (async () => {
5400 let items;
5401 try {
5402 items = await loadSubItems(entity, postId, relation);
5403 } catch (err) {
5404 hideLoadingSkeleton(tiles);
5405 renderListError(
5406 tiles,
5407 err instanceof Error ? err.message : __("Unknown error.", "desktop-mode")
5408 );
5409 return;
5410 }
5411 if (state.route.kind !== "sub-list" || state.route.postId !== postId || state.route.relation !== relation) {
5412 return;
5413 }
5414 hideLoadingSkeleton(tiles);
5415 paintStatus(
5416 state,
5417 [
5418 {
5419 id: "count",
5420 label: pluralLabel(items.length, "item", "items"),
5421 align: "start",
5422 sort: 10
5423 }
5424 ],
5425 {
5426 view: "sub-list",
5427 entityId: entity.id,
5428 postId,
5429 relation
5430 }
5431 );
5432 if (items.length === 0) {
5433 renderListEmptyMessage(
5434 tiles,
5435 emptySubListMessage(relation)
5436 );
5437 return;
5438 }
5439 let selectedKey = null;
5440 let selectedTile = null;
5441 for (const item of items) {
5442 const tile = buildIconTile({
5443 role: "entry",
5444 icon: item.icon,
5445 label: item.label
5446 });
5447 tile.dataset.subItemId = item.id;
5448 const tileKey = `sub:${item.id}`;
5449 layout.place(tile, tileKey, {
5450 name: item.label,
5451 date: item.date
5452 });
5453 tile.addEventListener("click", () => {
5454 if (selectedTile) {
5455 selectedTile.classList.remove(
5456 "desktop-mode-file-tile--selected"
5457 );
5458 }
5459 tile.classList.add(
5460 "desktop-mode-file-tile--selected"
5461 );
5462 selectedTile = tile;
5463 selectedKey = tileKey;
5464 showPreviewLoading(right);
5465 Promise.resolve(item.preview()).then((node) => {
5466 if (selectedKey !== tileKey) {
5467 return;
5468 }
5469 right.replaceChildren(node);
5470 }).catch((err) => {
5471 if (selectedKey !== tileKey) {
5472 return;
5473 }
5474 showPreviewError(right, err);
5475 });
5476 });
5477 tiles.appendChild(tile);
5478 }
5479 })();
5480 }
5481 function renderListEmptyMessage(host, message) {
5482 const empty = document.createElement("div");
5483 empty.className = "desktop-mode-my-wordpress__empty";
5484 empty.textContent = message;
5485 host.appendChild(empty);
5486 }
5487 function emptySubListMessage(relation) {
5488 switch (relation) {
5489 case "comments":
5490 return __("No comments on this post yet.", "desktop-mode");
5491 case "categories":
5492 return __("No categories assigned.", "desktop-mode");
5493 case "tags":
5494 return __("No tags assigned.", "desktop-mode");
5495 case "media":
5496 return __("No media attached to this post.", "desktop-mode");
5497 case "revisions":
5498 return __("No revisions yet.", "desktop-mode");
5499 case "author":
5500 return __("No author available.", "desktop-mode");
5501 case "contributors":
5502 return __("No additional contributors.", "desktop-mode");
5503 default:
5504 return __("Nothing to show.", "desktop-mode");
5505 }
5506 }
5507 async function loadSubItems(entity, postId, relation) {
5508 if (relation === "comments") {
5509 const comments = await fetchComments(postId);
5510 return comments.map(commentToView);
5511 }
5512 if (relation === "media") {
5513 const detail = await fetchEntityDetail(entity, postId);
5514 const ids = /* @__PURE__ */ new Set();
5515 if (detail.featured_media && detail.featured_media > 0) {
5516 ids.add(detail.featured_media);
5517 }
5518 const serverList = detail.desktop_mode_attached_media;
5519 if (Array.isArray(serverList) && serverList.length > 0) {
5520 for (const id of serverList) {
5521 if (typeof id === "number" && id > 0) {
5522 ids.add(id);
5523 }
5524 }
5525 } else {
5526 extractContentMediaIds(detail.content?.rendered ?? "").forEach(
5527 (id) => ids.add(id)
5528 );
5529 }
5530 const [batched, parentAttached] = await Promise.all([
5531 fetchMediaByIds(Array.from(ids)).catch(() => []),
5532 fetchAttachedMedia(postId).catch(() => [])
5533 ]);
5534 const seen = /* @__PURE__ */ new Set();
5535 const merged = [];
5536 const featuredId = detail.featured_media ?? 0;
5537 const orderedFromBatch = batched.slice().sort((a, b) => {
5538 if (a.id === featuredId && b.id !== featuredId) {
5539 return -1;
5540 }
5541 if (b.id === featuredId && a.id !== featuredId) {
5542 return 1;
5543 }
5544 return 0;
5545 });
5546 for (const m of [...orderedFromBatch, ...parentAttached]) {
5547 if (seen.has(m.id)) {
5548 continue;
5549 }
5550 seen.add(m.id);
5551 merged.push(m);
5552 }
5553 return merged.map(mediaToView);
5554 }
5555 if (relation === "categories" || relation === "tags") {
5556 const detail = await fetchEntityDetail(entity, postId);
5557 const ids = relation === "categories" ? detail.categories ?? [] : detail.tags ?? [];
5558 const terms = await fetchTerms(
5559 relation === "categories" ? "categories" : "tags",
5560 ids
5561 );
5562 return terms.map(termToView);
5563 }
5564 if (relation === "author") {
5565 const detail = await fetchEntityDetail(entity, postId);
5566 if (!detail.author) {
5567 return [];
5568 }
5569 const user = await fetchUser(detail.author);
5570 return [userToView(user)];
5571 }
5572 if (relation === "contributors") {
5573 const detail = await fetchEntityDetail(entity, postId);
5574 const contribs = detail.desktop_mode_contributors ?? [];
5575 return contribs.map(contributorToView);
5576 }
5577 if (relation === "revisions") {
5578 const revs = await fetchRevisions(entity, postId);
5579 const ordered = revs.slice().sort((a, b) => {
5580 const ta = Date.parse(a.modified || a.date || "");
5581 const tb = Date.parse(b.modified || b.date || "");
5582 return tb - ta;
5583 });
5584 return ordered.map((r) => revisionToView(r, entity, postId));
5585 }
5586 return [];
5587 }
5588 function commentToView(c) {
5589 const author = c.author_name || __("Anonymous", "desktop-mode");
5590 return {
5591 id: `comment:${c.id}`,
5592 icon: "dashicons-admin-comments",
5593 label: author,
5594 date: c.date,
5595 preview: async () => renderCommentDossier(c)
5596 };
5597 }
5598 async function renderCommentDossier(c) {
5599 let stats = null;
5600 try {
5601 stats = await fetchCommentStats(c.id);
5602 } catch {
5603 stats = null;
5604 }
5605 const wrap = document.createElement("div");
5606 wrap.className = "desktop-mode-my-wordpress__article desktop-mode-my-wordpress__comment";
5607 if (!stats) {
5608 appendCommentHeader(wrap, {
5609 authorName: c.author_name || __("Anonymous", "desktop-mode"),
5610 avatarUrl: c.author_avatar_urls ? pickAvatar(c.author_avatar_urls) ?? "" : "",
5611 authorLink: "",
5612 authorWebsite: "",
5613 status: c.status || "approved",
5614 date: c.date,
5615 editLink: "",
5616 totalApproved: 0
5617 });
5618 const body2 = document.createElement("div");
5619 body2.className = "desktop-mode-my-wordpress__article-content desktop-mode-my-wordpress__comment-body";
5620 body2.innerHTML = c.content.rendered;
5621 wrap.appendChild(body2);
5622 return wrap;
5623 }
5624 const { author, comment, post, parent, replies } = stats;
5625 appendCommentHeader(wrap, {
5626 authorName: author.displayName || author.name || __("Anonymous", "desktop-mode"),
5627 avatarUrl: author.avatarUrl,
5628 authorLink: author.profileLink ?? "",
5629 authorWebsite: author.url ?? "",
5630 status: comment.status,
5631 date: comment.date,
5632 editLink: comment.editLink,
5633 totalApproved: author.totalApprovedComments
5634 });
5635 if (parent) {
5636 const quote = document.createElement("blockquote");
5637 quote.className = "desktop-mode-my-wordpress__comment-quote";
5638 const lead = document.createElement("div");
5639 lead.className = "desktop-mode-my-wordpress__comment-quote-lead";
5640 lead.textContent = sprintf(
5641 // translators: %s is the parent comment's author name.
5642 __("In reply to %s", "desktop-mode"),
5643 parent.authorName
5644 );
5645 quote.appendChild(lead);
5646 const excerpt = document.createElement("p");
5647 excerpt.textContent = parent.excerpt || "";
5648 quote.appendChild(excerpt);
5649 wrap.appendChild(quote);
5650 }
5651 const body = document.createElement("div");
5652 body.className = "desktop-mode-my-wordpress__article-content desktop-mode-my-wordpress__comment-body";
5653 body.innerHTML = comment.rendered;
5654 wrap.appendChild(body);
5655 if (post) {
5656 const section = document.createElement("section");
5657 section.className = "desktop-mode-my-wordpress__user-section";
5658 const h = document.createElement("h3");
5659 h.textContent = __("On post", "desktop-mode");
5660 section.appendChild(h);
5661 const card = document.createElement("div");
5662 card.className = "desktop-mode-my-wordpress__comment-post";
5663 const titleEl = document.createElement("a");
5664 titleEl.className = "desktop-mode-my-wordpress__comment-post-title";
5665 titleEl.href = post.link;
5666 titleEl.target = "_blank";
5667 titleEl.rel = "noopener noreferrer";
5668 titleEl.textContent = post.title || `#${post.id}`;
5669 card.appendChild(titleEl);
5670 const meta = document.createElement("div");
5671 meta.className = "desktop-mode-my-wordpress__comment-post-meta";
5672 const parts = [];
5673 parts.push(formatDate(post.date));
5674 if (post.author?.name) {
5675 parts.push(post.author.name);
5676 }
5677 if (post.status && post.status !== "publish") {
5678 parts.push(post.status);
5679 }
5680 meta.textContent = parts.join(" · ");
5681 card.appendChild(meta);
5682 section.appendChild(card);
5683 wrap.appendChild(section);
5684 }
5685 if (replies.length > 0) {
5686 const section = document.createElement("section");
5687 section.className = "desktop-mode-my-wordpress__user-section";
5688 const h = document.createElement("h3");
5689 h.textContent = sprintf(
5690 // translators: %d is the number of direct replies to a comment.
5691 _n("Reply (%d)", "Replies (%d)", replies.length),
5692 replies.length
5693 );
5694 section.appendChild(h);
5695 const list = document.createElement("ul");
5696 list.className = "desktop-mode-my-wordpress__comment-replies";
5697 for (const r of replies) {
5698 const li = document.createElement("li");
5699 li.className = "desktop-mode-my-wordpress__comment-reply";
5700 if (r.avatarUrl) {
5701 const img = document.createElement("img");
5702 img.src = r.avatarUrl;
5703 img.alt = "";
5704 img.className = "desktop-mode-my-wordpress__comment-reply-avatar";
5705 li.appendChild(img);
5706 }
5707 const txt = document.createElement("div");
5708 txt.className = "desktop-mode-my-wordpress__comment-reply-text";
5709 const head = document.createElement("div");
5710 head.className = "desktop-mode-my-wordpress__comment-reply-head";
5711 const who = document.createElement("span");
5712 who.className = "desktop-mode-my-wordpress__comment-reply-name";
5713 who.textContent = r.authorName || __("Anonymous", "desktop-mode");
5714 head.appendChild(who);
5715 const when = document.createElement("span");
5716 when.className = "desktop-mode-my-wordpress__comment-reply-when";
5717 when.textContent = formatDate(r.date);
5718 head.appendChild(when);
5719 txt.appendChild(head);
5720 const ex = document.createElement("p");
5721 ex.className = "desktop-mode-my-wordpress__comment-reply-excerpt";
5722 ex.textContent = r.excerpt || "";
5723 txt.appendChild(ex);
5724 li.appendChild(txt);
5725 list.appendChild(li);
5726 }
5727 section.appendChild(list);
5728 wrap.appendChild(section);
5729 }
5730 if (comment.ip || comment.userAgent) {
5731 const dl = document.createElement("dl");
5732 dl.className = "desktop-mode-my-wordpress__user-milestones";
5733 if (comment.ip) {
5734 const dt = document.createElement("dt");
5735 dt.textContent = __("IP", "desktop-mode");
5736 dl.appendChild(dt);
5737 const dd = document.createElement("dd");
5738 dd.textContent = comment.ip;
5739 dl.appendChild(dd);
5740 }
5741 if (comment.userAgent) {
5742 const dt = document.createElement("dt");
5743 dt.textContent = __("User agent", "desktop-mode");
5744 dl.appendChild(dt);
5745 const dd = document.createElement("dd");
5746 dd.textContent = comment.userAgent;
5747 dl.appendChild(dd);
5748 }
5749 wrap.appendChild(dl);
5750 }
5751 return wrap;
5752 }
5753 function appendCommentHeader(host, header) {
5754 const wrap = document.createElement("header");
5755 wrap.className = "desktop-mode-my-wordpress__user-header";
5756 if (header.avatarUrl) {
5757 const img = document.createElement("img");
5758 img.src = header.avatarUrl;
5759 img.alt = "";
5760 img.className = "desktop-mode-my-wordpress__user-avatar";
5761 wrap.appendChild(img);
5762 }
5763 const right = document.createElement("div");
5764 right.className = "desktop-mode-my-wordpress__user-headline";
5765 const h = document.createElement("h2");
5766 h.className = "desktop-mode-my-wordpress__article-title";
5767 h.textContent = header.authorName;
5768 right.appendChild(h);
5769 const badges = document.createElement("div");
5770 badges.className = "desktop-mode-my-wordpress__user-roles";
5771 const status = document.createElement("span");
5772 status.className = "desktop-mode-my-wordpress__user-role desktop-mode-my-wordpress__comment-status--" + (header.status || "approved");
5773 status.textContent = header.status || "approved";
5774 badges.appendChild(status);
5775 const dateBadge = document.createElement("span");
5776 dateBadge.className = "desktop-mode-my-wordpress__user-role desktop-mode-my-wordpress__comment-date-badge";
5777 dateBadge.textContent = formatDate(header.date);
5778 badges.appendChild(dateBadge);
5779 if (header.totalApproved > 1) {
5780 const totalBadge = document.createElement("span");
5781 totalBadge.className = "desktop-mode-my-wordpress__user-role";
5782 totalBadge.textContent = sprintf(
5783 // translators: %d is a comment count for a particular author.
5784 _n(
5785 "%d comment site-wide",
5786 "%d comments site-wide",
5787 header.totalApproved
5788 ),
5789 header.totalApproved
5790 );
5791 badges.appendChild(totalBadge);
5792 }
5793 right.appendChild(badges);
5794 const links = document.createElement("div");
5795 links.className = "desktop-mode-my-wordpress__user-links";
5796 if (header.authorLink) {
5797 const a = document.createElement("a");
5798 a.href = header.authorLink;
5799 a.target = "_blank";
5800 a.rel = "noopener noreferrer";
5801 a.textContent = __("Author archive", "desktop-mode");
5802 links.appendChild(a);
5803 }
5804 if (header.authorWebsite) {
5805 const a = document.createElement("a");
5806 a.href = header.authorWebsite;
5807 a.target = "_blank";
5808 a.rel = "noopener noreferrer";
5809 a.textContent = __("Website", "desktop-mode");
5810 links.appendChild(a);
5811 }
5812 if (header.editLink) {
5813 const a = document.createElement("a");
5814 a.href = header.editLink;
5815 a.target = "_blank";
5816 a.rel = "noopener noreferrer";
5817 a.textContent = __("Moderate", "desktop-mode");
5818 links.appendChild(a);
5819 }
5820 if (links.childElementCount > 0) {
5821 right.appendChild(links);
5822 }
5823 wrap.appendChild(right);
5824 host.appendChild(wrap);
5825 }
5826 function userToView(u) {
5827 return {
5828 id: `user:${u.id}`,
5829 icon: "dashicons-admin-users",
5830 label: u.name || u.slug || `#${u.id}`,
5831 date: (/* @__PURE__ */ new Date(0)).toISOString(),
5832 preview: async () => {
5833 const fallbackName = u.name || u.slug || `#${u.id}`;
5834 const fallbackAvatar = pickAvatar(u.avatar_urls) ?? "";
5835 return renderUserDossier({
5836 userId: u.id,
5837 fallbackName,
5838 fallbackAvatar,
5839 fallbackDescription: u.description ?? ""
5840 });
5841 }
5842 };
5843 }
5844 function contributorToView(c) {
5845 return {
5846 id: `contributor:${c.userId}`,
5847 icon: "dashicons-admin-users",
5848 label: c.userName || `#${c.userId}`,
5849 date: (/* @__PURE__ */ new Date(0)).toISOString(),
5850 preview: async () => renderUserDossier({
5851 userId: c.userId,
5852 fallbackName: c.userName,
5853 fallbackAvatar: c.userAvatarUrl,
5854 fallbackDescription: ""
5855 })
5856 };
5857 }
5858 async function renderUserDossier(opts) {
5859 let stats = null;
5860 try {
5861 stats = await fetchUserStats(opts.userId);
5862 } catch {
5863 stats = null;
5864 }
5865 const wrap = document.createElement("div");
5866 wrap.className = "desktop-mode-my-wordpress__article desktop-mode-my-wordpress__user";
5867 if (!stats) {
5868 let basic = null;
5869 try {
5870 basic = await fetchUser(opts.userId);
5871 } catch {
5872 basic = null;
5873 }
5874 appendUserHeader(wrap, {
5875 name: basic?.name ?? opts.fallbackName,
5876 avatarUrl: basic && pickAvatar(basic.avatar_urls) || opts.fallbackAvatar,
5877 roles: [],
5878 website: "",
5879 link: basic?.link ?? ""
5880 });
5881 const desc = basic?.description ?? opts.fallbackDescription;
5882 if (desc) {
5883 const bio = document.createElement("div");
5884 bio.className = "desktop-mode-my-wordpress__user-bio";
5885 bio.textContent = desc;
5886 wrap.appendChild(bio);
5887 }
5888 return wrap;
5889 }
5890 const { profile, counts, recent, topTerms, milestones, activity: activity2 } = stats;
5891 appendUserHeader(wrap, {
5892 name: profile.name || opts.fallbackName,
5893 avatarUrl: profile.avatarUrl || opts.fallbackAvatar,
5894 roles: profile.roleLabels ?? [],
5895 website: profile.website,
5896 link: profile.link
5897 });
5898 if (profile.description) {
5899 const bio = document.createElement("div");
5900 bio.className = "desktop-mode-my-wordpress__user-bio";
5901 bio.textContent = profile.description;
5902 wrap.appendChild(bio);
5903 }
5904 const cards = document.createElement("div");
5905 cards.className = "desktop-mode-my-wordpress__user-stats";
5906 cards.appendChild(
5907 buildStatCard(
5908 counts.posts.total.toLocaleString(),
5909 __("Posts", "desktop-mode"),
5910 counts.posts.publish > 0 ? sprintf(
5911 // translators: %d is a published-post count.
5912 __("%d published", "desktop-mode"),
5913 counts.posts.publish
5914 ) : ""
5915 )
5916 );
5917 cards.appendChild(
5918 buildStatCard(
5919 counts.pages.total.toLocaleString(),
5920 __("Pages", "desktop-mode"),
5921 counts.pages.publish > 0 ? sprintf(
5922 // translators: %d is a published-page count.
5923 __("%d published", "desktop-mode"),
5924 counts.pages.publish
5925 ) : ""
5926 )
5927 );
5928 cards.appendChild(
5929 buildStatCard(
5930 counts.commentsReceived.toLocaleString(),
5931 __("Comments received", "desktop-mode"),
5932 ""
5933 )
5934 );
5935 cards.appendChild(
5936 buildStatCard(
5937 counts.commentsLeft.toLocaleString(),
5938 __("Comments left", "desktop-mode"),
5939 ""
5940 )
5941 );
5942 wrap.appendChild(cards);
5943 const spark = buildActivitySparkline(activity2);
5944 if (spark) {
5945 wrap.appendChild(spark);
5946 }
5947 const milestoneRow = buildMilestonesRow(profile, milestones);
5948 if (milestoneRow) {
5949 wrap.appendChild(milestoneRow);
5950 }
5951 if (recent.length > 0) {
5952 const section = document.createElement("section");
5953 section.className = "desktop-mode-my-wordpress__user-section";
5954 const h = document.createElement("h3");
5955 h.textContent = __("Recent posts", "desktop-mode");
5956 section.appendChild(h);
5957 const ul = document.createElement("ul");
5958 ul.className = "desktop-mode-my-wordpress__user-recent";
5959 for (const r of recent) {
5960 const li = document.createElement("li");
5961 const a = document.createElement("a");
5962 a.href = r.link;
5963 a.target = "_blank";
5964 a.rel = "noopener noreferrer";
5965 a.textContent = r.title || `#${r.id}`;
5966 li.appendChild(a);
5967 const meta = document.createElement("span");
5968 meta.className = "desktop-mode-my-wordpress__user-recent-meta";
5969 meta.textContent = `${formatDate(r.date)} · ${r.status}`;
5970 li.appendChild(meta);
5971 ul.appendChild(li);
5972 }
5973 section.appendChild(ul);
5974 wrap.appendChild(section);
5975 }
5976 if (topTerms.length > 0) {
5977 const section = document.createElement("section");
5978 section.className = "desktop-mode-my-wordpress__user-section";
5979 const h = document.createElement("h3");
5980 h.textContent = __("Top categories & tags", "desktop-mode");
5981 section.appendChild(h);
5982 const chips = document.createElement("div");
5983 chips.className = "desktop-mode-my-wordpress__user-chips";
5984 for (const t of topTerms) {
5985 const chip = document.createElement("span");
5986 chip.className = "desktop-mode-my-wordpress__user-chip " + (t.taxonomy === "post_tag" ? "desktop-mode-my-wordpress__user-chip--tag" : "desktop-mode-my-wordpress__user-chip--category");
5987 const name = document.createElement("span");
5988 name.textContent = t.name;
5989 chip.appendChild(name);
5990 const count = document.createElement("span");
5991 count.className = "desktop-mode-my-wordpress__user-chip-count";
5992 count.textContent = String(t.count);
5993 chip.appendChild(count);
5994 chips.appendChild(chip);
5995 }
5996 section.appendChild(chips);
5997 wrap.appendChild(section);
5998 }
5999 return wrap;
6000 }
6001 function appendUserHeader(host, header) {
6002 const wrap = document.createElement("header");
6003 wrap.className = "desktop-mode-my-wordpress__user-header";
6004 if (header.avatarUrl) {
6005 const img = document.createElement("img");
6006 img.src = header.avatarUrl;
6007 img.alt = "";
6008 img.className = "desktop-mode-my-wordpress__user-avatar";
6009 wrap.appendChild(img);
6010 }
6011 const right = document.createElement("div");
6012 right.className = "desktop-mode-my-wordpress__user-headline";
6013 const h = document.createElement("h2");
6014 h.className = "desktop-mode-my-wordpress__article-title";
6015 h.textContent = header.name;
6016 right.appendChild(h);
6017 if (header.roles.length > 0) {
6018 const rolesRow = document.createElement("div");
6019 rolesRow.className = "desktop-mode-my-wordpress__user-roles";
6020 for (const r of header.roles) {
6021 const badge = document.createElement("span");
6022 badge.className = "desktop-mode-my-wordpress__user-role";
6023 badge.textContent = r;
6024 rolesRow.appendChild(badge);
6025 }
6026 right.appendChild(rolesRow);
6027 }
6028 const links = document.createElement("div");
6029 links.className = "desktop-mode-my-wordpress__user-links";
6030 if (header.link) {
6031 const a = document.createElement("a");
6032 a.href = header.link;
6033 a.target = "_blank";
6034 a.rel = "noopener noreferrer";
6035 a.textContent = __("Author archive", "desktop-mode");
6036 links.appendChild(a);
6037 }
6038 if (header.website) {
6039 const a = document.createElement("a");
6040 a.href = header.website;
6041 a.target = "_blank";
6042 a.rel = "noopener noreferrer";
6043 a.textContent = __("Website", "desktop-mode");
6044 links.appendChild(a);
6045 }
6046 if (links.childElementCount > 0) {
6047 right.appendChild(links);
6048 }
6049 wrap.appendChild(right);
6050 host.appendChild(wrap);
6051 }
6052 function buildStatCard(value, label, caption) {
6053 const card = document.createElement("div");
6054 card.className = "desktop-mode-my-wordpress__user-stat";
6055 const v = document.createElement("span");
6056 v.className = "desktop-mode-my-wordpress__user-stat-value";
6057 v.textContent = value;
6058 card.appendChild(v);
6059 const l = document.createElement("span");
6060 l.className = "desktop-mode-my-wordpress__user-stat-label";
6061 l.textContent = label;
6062 card.appendChild(l);
6063 if (caption) {
6064 const c = document.createElement("span");
6065 c.className = "desktop-mode-my-wordpress__user-stat-caption";
6066 c.textContent = caption;
6067 card.appendChild(c);
6068 }
6069 return card;
6070 }
6071 function buildActivitySparkline(activity2) {
6072 if (activity2.length === 0) {
6073 return null;
6074 }
6075 const now = /* @__PURE__ */ new Date();
6076 const months = [];
6077 for (let i = 11; i >= 0; i -= 1) {
6078 const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
6079 const ym = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
6080 const found = activity2.find((a) => a.ym === ym);
6081 months.push({
6082 ym,
6083 count: found?.count ?? 0,
6084 label: d.toLocaleString(void 0, { month: "short" })
6085 });
6086 }
6087 const max = Math.max(1, ...months.map((m) => m.count));
6088 const wrap = document.createElement("section");
6089 wrap.className = "desktop-mode-my-wordpress__user-section desktop-mode-my-wordpress__user-spark";
6090 const h = document.createElement("h3");
6091 h.textContent = __("Activity (last 12 months)", "desktop-mode");
6092 wrap.appendChild(h);
6093 const chart = document.createElement("div");
6094 chart.className = "desktop-mode-my-wordpress__user-spark-chart";
6095 for (const m of months) {
6096 const col = document.createElement("div");
6097 col.className = "desktop-mode-my-wordpress__user-spark-col";
6098 const bar = document.createElement("div");
6099 bar.className = "desktop-mode-my-wordpress__user-spark-bar";
6100 bar.style.height = `${Math.round(m.count / max * 100)}%`;
6101 bar.title = sprintf(
6102 // translators: 1: month label, 2: post count.
6103 __("%1$s · %2$d posts", "desktop-mode"),
6104 m.label,
6105 m.count
6106 );
6107 if (m.count === 0) {
6108 bar.classList.add("desktop-mode-my-wordpress__user-spark-bar--empty");
6109 }
6110 col.appendChild(bar);
6111 const lbl = document.createElement("span");
6112 lbl.className = "desktop-mode-my-wordpress__user-spark-label";
6113 lbl.textContent = m.label;
6114 col.appendChild(lbl);
6115 chart.appendChild(col);
6116 }
6117 wrap.appendChild(chart);
6118 return wrap;
6119 }
6120 function buildMilestonesRow(profile, milestones) {
6121 const items = [];
6122 if (profile.registered) {
6123 items.push({
6124 label: __("Member since", "desktop-mode"),
6125 value: formatYearMonth(profile.registered)
6126 });
6127 }
6128 if (milestones.firstPublished) {
6129 items.push({
6130 label: __("First published", "desktop-mode"),
6131 value: formatYearMonth(milestones.firstPublished)
6132 });
6133 }
6134 if (milestones.lastPublished) {
6135 items.push({
6136 label: __("Last published", "desktop-mode"),
6137 value: formatYearMonth(milestones.lastPublished)
6138 });
6139 }
6140 if (items.length === 0) {
6141 return null;
6142 }
6143 const dl = document.createElement("dl");
6144 dl.className = "desktop-mode-my-wordpress__user-milestones";
6145 for (const item of items) {
6146 const dt = document.createElement("dt");
6147 dt.textContent = item.label;
6148 dl.appendChild(dt);
6149 const dd = document.createElement("dd");
6150 dd.textContent = item.value;
6151 dl.appendChild(dd);
6152 }
6153 return dl;
6154 }
6155 function formatYearMonth(iso) {
6156 if (!iso) {
6157 return "";
6158 }
6159 try {
6160 return new Date(iso).toLocaleString(void 0, {
6161 year: "numeric",
6162 month: "long"
6163 });
6164 } catch {
6165 return iso;
6166 }
6167 }
6168 function pickAvatar(avatars) {
6169 if (!avatars) {
6170 return null;
6171 }
6172 return avatars["96"] ?? avatars["48"] ?? avatars["24"] ?? Object.values(avatars)[0] ?? null;
6173 }
6174 function termToView(t) {
6175 return {
6176 id: `term:${t.id}`,
6177 icon: t.taxonomy === "post_tag" ? "dashicons-tag" : "dashicons-category",
6178 label: t.name,
6179 date: (/* @__PURE__ */ new Date(0)).toISOString(),
6180 preview: async () => renderTermDossier(t)
6181 };
6182 }
6183 async function renderTermDossier(t) {
6184 let stats = null;
6185 try {
6186 stats = await fetchTermStats(t.taxonomy, t.id);
6187 } catch {
6188 stats = null;
6189 }
6190 const wrap = document.createElement("div");
6191 wrap.className = "desktop-mode-my-wordpress__article desktop-mode-my-wordpress__term";
6192 if (!stats) {
6193 appendTermHeader(wrap, {
6194 name: t.name,
6195 taxonomyLabel: t.taxonomy,
6196 isTag: t.taxonomy === "post_tag",
6197 count: t.count ?? 0,
6198 link: t.link ?? "",
6199 parentName: ""
6200 });
6201 if (t.description) {
6202 const body = document.createElement("div");
6203 body.className = "desktop-mode-my-wordpress__user-bio";
6204 body.innerHTML = t.description;
6205 wrap.appendChild(body);
6206 }
6207 return wrap;
6208 }
6209 const { profile, counts, recent, topAuthors, coTerms, milestones, activity: activity2 } = stats;
6210 appendTermHeader(wrap, {
6211 name: profile.name,
6212 taxonomyLabel: profile.taxonomyLabel || profile.taxonomy,
6213 isTag: profile.taxonomy === "post_tag",
6214 count: profile.storedCount,
6215 link: profile.link,
6216 parentName: profile.parentName ?? ""
6217 });
6218 if (profile.description) {
6219 const bio = document.createElement("div");
6220 bio.className = "desktop-mode-my-wordpress__user-bio";
6221 bio.innerHTML = profile.description;
6222 wrap.appendChild(bio);
6223 }
6224 const cards = document.createElement("div");
6225 cards.className = "desktop-mode-my-wordpress__user-stats";
6226 cards.appendChild(
6227 buildStatCard(
6228 counts.posts.total.toLocaleString(),
6229 __("Posts", "desktop-mode"),
6230 counts.posts.publish > 0 ? sprintf(
6231 // translators: %d is a published-post count.
6232 __("%d published", "desktop-mode"),
6233 counts.posts.publish
6234 ) : ""
6235 )
6236 );
6237 cards.appendChild(
6238 buildStatCard(
6239 counts.commentsReceived.toLocaleString(),
6240 __("Comments", "desktop-mode"),
6241 ""
6242 )
6243 );
6244 cards.appendChild(
6245 buildStatCard(
6246 counts.distinctAuthors.toLocaleString(),
6247 __("Authors", "desktop-mode"),
6248 counts.distinctAuthors === 1 ? __("one contributor", "desktop-mode") : ""
6249 )
6250 );
6251 wrap.appendChild(cards);
6252 const spark = buildActivitySparkline(activity2);
6253 if (spark) {
6254 wrap.appendChild(spark);
6255 }
6256 const milestoneRow = buildTermMilestonesRow(milestones);
6257 if (milestoneRow) {
6258 wrap.appendChild(milestoneRow);
6259 }
6260 if (topAuthors.length > 0) {
6261 const section = document.createElement("section");
6262 section.className = "desktop-mode-my-wordpress__user-section";
6263 const h = document.createElement("h3");
6264 h.textContent = __("Top contributors", "desktop-mode");
6265 section.appendChild(h);
6266 const grid = document.createElement("div");
6267 grid.className = "desktop-mode-my-wordpress__term-authors";
6268 for (const a of topAuthors) {
6269 const card = document.createElement("div");
6270 card.className = "desktop-mode-my-wordpress__term-author";
6271 if (a.userAvatarUrl) {
6272 const img = document.createElement("img");
6273 img.src = a.userAvatarUrl;
6274 img.alt = "";
6275 img.className = "desktop-mode-my-wordpress__term-author-avatar";
6276 card.appendChild(img);
6277 }
6278 const text = document.createElement("div");
6279 text.className = "desktop-mode-my-wordpress__term-author-text";
6280 const name = document.createElement("span");
6281 name.className = "desktop-mode-my-wordpress__term-author-name";
6282 name.textContent = a.userName;
6283 text.appendChild(name);
6284 const count = document.createElement("span");
6285 count.className = "desktop-mode-my-wordpress__term-author-count";
6286 count.textContent = sprintf(
6287 // translators: %d is a post count.
6288 _n("%d post", "%d posts", a.count),
6289 a.count
6290 );
6291 text.appendChild(count);
6292 card.appendChild(text);
6293 grid.appendChild(card);
6294 }
6295 section.appendChild(grid);
6296 wrap.appendChild(section);
6297 }
6298 if (recent.length > 0) {
6299 const section = document.createElement("section");
6300 section.className = "desktop-mode-my-wordpress__user-section";
6301 const h = document.createElement("h3");
6302 h.textContent = __("Recent posts", "desktop-mode");
6303 section.appendChild(h);
6304 const ul = document.createElement("ul");
6305 ul.className = "desktop-mode-my-wordpress__user-recent";
6306 for (const r of recent) {
6307 const li = document.createElement("li");
6308 const a = document.createElement("a");
6309 a.href = r.link;
6310 a.target = "_blank";
6311 a.rel = "noopener noreferrer";
6312 a.textContent = r.title || `#${r.id}`;
6313 li.appendChild(a);
6314 const meta = document.createElement("span");
6315 meta.className = "desktop-mode-my-wordpress__user-recent-meta";
6316 meta.textContent = `${formatDate(r.date)} · ${r.status}${r.author?.name ? " · " + r.author.name : ""}`;
6317 li.appendChild(meta);
6318 ul.appendChild(li);
6319 }
6320 section.appendChild(ul);
6321 wrap.appendChild(section);
6322 }
6323 if (coTerms.length > 0) {
6324 const section = document.createElement("section");
6325 section.className = "desktop-mode-my-wordpress__user-section";
6326 const h = document.createElement("h3");
6327 h.textContent = profile.taxonomy === "post_tag" ? __("Often paired tags", "desktop-mode") : __("Often paired categories", "desktop-mode");
6328 section.appendChild(h);
6329 const chips = document.createElement("div");
6330 chips.className = "desktop-mode-my-wordpress__user-chips";
6331 for (const co of coTerms) {
6332 const chip = document.createElement("span");
6333 chip.className = "desktop-mode-my-wordpress__user-chip " + (profile.taxonomy === "post_tag" ? "desktop-mode-my-wordpress__user-chip--tag" : "desktop-mode-my-wordpress__user-chip--category");
6334 const name = document.createElement("span");
6335 name.textContent = co.name;
6336 chip.appendChild(name);
6337 const count = document.createElement("span");
6338 count.className = "desktop-mode-my-wordpress__user-chip-count";
6339 count.textContent = String(co.count);
6340 chip.appendChild(count);
6341 chips.appendChild(chip);
6342 }
6343 section.appendChild(chips);
6344 wrap.appendChild(section);
6345 }
6346 return wrap;
6347 }
6348 function appendTermHeader(host, header) {
6349 const wrap = document.createElement("header");
6350 wrap.className = "desktop-mode-my-wordpress__term-header";
6351 const iconHost = document.createElement("span");
6352 iconHost.className = "desktop-mode-my-wordpress__term-icon " + (header.isTag ? "desktop-mode-my-wordpress__term-icon--tag" : "desktop-mode-my-wordpress__term-icon--category");
6353 const iconGlyph = document.createElement("span");
6354 iconGlyph.style.cssText = "font-family:dashicons;font-size:32px;line-height:1;display:inline-block;";
6355 iconGlyph.textContent = header.isTag ? "" : "";
6356 iconHost.appendChild(iconGlyph);
6357 wrap.appendChild(iconHost);
6358 const right = document.createElement("div");
6359 right.className = "desktop-mode-my-wordpress__user-headline";
6360 const h = document.createElement("h2");
6361 h.className = "desktop-mode-my-wordpress__article-title";
6362 h.textContent = header.name;
6363 right.appendChild(h);
6364 const meta = document.createElement("div");
6365 meta.className = "desktop-mode-my-wordpress__user-roles";
6366 const taxBadge = document.createElement("span");
6367 taxBadge.className = "desktop-mode-my-wordpress__user-role " + (header.isTag ? "desktop-mode-my-wordpress__user-role--tag" : "desktop-mode-my-wordpress__user-role--category");
6368 taxBadge.textContent = header.taxonomyLabel;
6369 meta.appendChild(taxBadge);
6370 if (header.parentName) {
6371 const parent = document.createElement("span");
6372 parent.className = "desktop-mode-my-wordpress__user-role";
6373 parent.textContent = sprintf(
6374 // translators: %s is the name of the parent category.
6375 __("in %s", "desktop-mode"),
6376 header.parentName
6377 );
6378 meta.appendChild(parent);
6379 }
6380 right.appendChild(meta);
6381 if (header.link) {
6382 const links = document.createElement("div");
6383 links.className = "desktop-mode-my-wordpress__user-links";
6384 const a = document.createElement("a");
6385 a.href = header.link;
6386 a.target = "_blank";
6387 a.rel = "noopener noreferrer";
6388 a.textContent = __("View archive", "desktop-mode");
6389 links.appendChild(a);
6390 right.appendChild(links);
6391 }
6392 wrap.appendChild(right);
6393 host.appendChild(wrap);
6394 }
6395 function buildTermMilestonesRow(milestones) {
6396 const items = [];
6397 if (milestones.firstPosted) {
6398 items.push({
6399 label: __("First post", "desktop-mode"),
6400 value: formatYearMonth(milestones.firstPosted)
6401 });
6402 }
6403 if (milestones.lastPosted) {
6404 items.push({
6405 label: __("Last post", "desktop-mode"),
6406 value: formatYearMonth(milestones.lastPosted)
6407 });
6408 }
6409 if (items.length === 0) {
6410 return null;
6411 }
6412 const dl = document.createElement("dl");
6413 dl.className = "desktop-mode-my-wordpress__user-milestones";
6414 for (const item of items) {
6415 const dt = document.createElement("dt");
6416 dt.textContent = item.label;
6417 dl.appendChild(dt);
6418 const dd = document.createElement("dd");
6419 dd.textContent = item.value;
6420 dl.appendChild(dd);
6421 }
6422 return dl;
6423 }
6424 function mediaToView(m) {
6425 const isImage = m.mime_type.startsWith("image/");
6426 return {
6427 id: `media:${m.id}`,
6428 icon: isImage ? "dashicons-format-image" : "dashicons-media-default",
6429 label: stripTags(m.title.rendered) || `#${m.id}`,
6430 date: m.date,
6431 preview: () => {
6432 const wrap = document.createElement("div");
6433 wrap.className = "desktop-mode-my-wordpress__article";
6434 const h = document.createElement("h2");
6435 h.className = "desktop-mode-my-wordpress__article-title";
6436 h.textContent = stripTags(m.title.rendered) || `#${m.id}`;
6437 wrap.appendChild(h);
6438 const meta = document.createElement("p");
6439 meta.className = "desktop-mode-my-wordpress__article-meta";
6440 meta.textContent = `${m.mime_type} · ${formatDate(m.date)}`;
6441 wrap.appendChild(meta);
6442 if (isImage) {
6443 const img = document.createElement("img");
6444 img.className = "desktop-mode-my-wordpress__article-hero";
6445 const sizes = m.media_details?.sizes;
6446 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? m.source_url;
6447 img.alt = m.alt_text ?? "";
6448 wrap.appendChild(img);
6449 } else {
6450 const link = document.createElement("p");
6451 const a = document.createElement("a");
6452 a.href = m.source_url;
6453 a.textContent = m.source_url;
6454 a.target = "_blank";
6455 a.rel = "noopener noreferrer";
6456 link.appendChild(a);
6457 wrap.appendChild(link);
6458 }
6459 return wrap;
6460 }
6461 };
6462 }
6463 function revisionToView(r, entity, postId) {
6464 const label = stripTags(r.title?.rendered ?? "") || formatDate(r.date);
6465 return {
6466 id: `revision:${r.id}`,
6467 icon: "dashicons-backup",
6468 label,
6469 date: r.modified || r.date,
6470 preview: async () => {
6471 let detail = null;
6472 try {
6473 detail = await fetchRevision(entity, postId, r.id);
6474 } catch {
6475 detail = null;
6476 }
6477 const wrap = document.createElement("article");
6478 wrap.className = "desktop-mode-my-wordpress__article";
6479 const h = document.createElement("h2");
6480 h.className = "desktop-mode-my-wordpress__article-title";
6481 h.textContent = stripTags(detail?.title?.rendered ?? r.title?.rendered ?? "") || label;
6482 wrap.appendChild(h);
6483 const meta = document.createElement("p");
6484 meta.className = "desktop-mode-my-wordpress__article-meta";
6485 meta.textContent = sprintf(
6486 // translators: %s is a formatted date.
6487 __("Saved %s", "desktop-mode"),
6488 formatDate(detail?.modified || detail?.date || r.modified || r.date)
6489 );
6490 wrap.appendChild(meta);
6491 const html2 = detail?.content?.rendered ?? "";
6492 if (html2) {
6493 const content = document.createElement("div");
6494 content.className = "desktop-mode-my-wordpress__article-content";
6495 content.innerHTML = html2;
6496 wrap.appendChild(content);
6497 } else {
6498 const empty = document.createElement("p");
6499 empty.className = "desktop-mode-my-wordpress__article-meta";
6500 empty.textContent = detail ? __("This revision has no rendered content.", "desktop-mode") : __(
6501 "Couldn’t load the revision content. You may not have permission to view it.",
6502 "desktop-mode"
6503 );
6504 wrap.appendChild(empty);
6505 }
6506 return wrap;
6507 }
6508 };
6509 }
6510 function formatDate(iso) {
6511 if (!iso) {
6512 return "";
6513 }
6514 try {
6515 return new Date(iso).toLocaleString();
6516 } catch {
6517 return iso;
6518 }
6519 }
6520 function openEditor(entity, id, title) {
6521 const url = buildEditUrl(id);
6522 openIframeWindow({
6523 id: `${entity.id}-edit-${id}`,
6524 url,
6525 title,
6526 icon: entity.icon
6527 });
6528 }
6529 function openTileMenu(state, ctx, entity, item, title, pos) {
6530 closeAnyTileMenu();
6531 const menu = document.createElement("wpd-context-menu");
6532 menu.setAttribute("open", "");
6533 menu.classList.add("desktop-mode-my-wordpress__menu");
6534 menu.style.left = `${pos.x}px`;
6535 menu.style.top = `${pos.y}px`;
6536 const addOption = (id, label, icon, danger = false) => {
6537 const opt = document.createElement("wpd-context-menu-option");
6538 opt.dataset.menuItemId = id;
6539 opt.setAttribute("value", id);
6540 opt.setAttribute("icon", sanitizeClass(icon));
6541 if (danger) {
6542 opt.setAttribute("danger", "");
6543 }
6544 opt.textContent = label;
6545 menu.appendChild(opt);
6546 };
6547 const baseOptions = [
6548 {
6549 id: "open",
6550 label: __("Open in editor", "desktop-mode"),
6551 icon: "dashicons-edit"
6552 },
6553 {
6554 id: "navigate-into",
6555 label: __("Navigate into", "desktop-mode"),
6556 icon: "dashicons-category"
6557 },
6558 {
6559 id: "trash",
6560 label: __("Move to Trash", "desktop-mode"),
6561 icon: "dashicons-trash",
6562 danger: true
6563 }
6564 ];
6565 const ctxFilter = {
6566 entityId: entity.id,
6567 kind: entity.kind ?? "post",
6568 item
6569 };
6570 const options = applyFilters(
6571 "desktop-mode.my-wordpress.tile-context-menu",
6572 baseOptions,
6573 ctxFilter
6574 );
6575 const finalOptions = Array.isArray(options) ? options : baseOptions;
6576 for (const o of finalOptions) {
6577 addOption(o.id, o.label, o.icon, o.danger);
6578 }
6579 menu.addEventListener("wpd-context-menu-pick", (e) => {
6580 const detail = e.detail;
6581 closeAnyTileMenu();
6582 if (detail.id === "open") {
6583 openEditor(entity, item.id, title);
6584 return;
6585 }
6586 if (detail.id === "navigate-into") {
6587 navigate(state, {
6588 kind: "detail",
6589 entityId: entity.id,
6590 postId: item.id,
6591 postTitle: title
6592 });
6593 return;
6594 }
6595 if (detail.id === "trash") {
6596 void confirmTrash(state, ctx, entity, item.id, title);
6597 return;
6598 }
6599 const match = finalOptions.find((o) => o.id === detail.id);
6600 if (match && typeof match.onSelect === "function") {
6601 try {
6602 match.onSelect();
6603 } catch (err) {
6604 console.error(
6605 `[my-wordpress] tile-context-menu '${detail.id}' onSelect threw:`,
6606 err
6607 );
6608 }
6609 }
6610 });
6611 document.body.appendChild(menu);
6612 const rect = menu.getBoundingClientRect();
6613 if (rect.right > window.innerWidth) {
6614 menu.style.left = `${Math.max(
6615 0,
6616 window.innerWidth - rect.width - 8
6617 )}px`;
6618 }
6619 if (rect.bottom > window.innerHeight) {
6620 menu.style.top = `${Math.max(
6621 0,
6622 window.innerHeight - rect.height - 8
6623 )}px`;
6624 }
6625 queueMicrotask(() => {
6626 const onDocPointerDown = (ev) => {
6627 const target = ev.target;
6628 if (target instanceof Node && menu.contains(target)) {
6629 return;
6630 }
6631 closeAnyTileMenu();
6632 };
6633 const onDocKey = (ev) => {
6634 if (ev.key === "Escape") {
6635 closeAnyTileMenu();
6636 }
6637 };
6638 document.addEventListener("pointerdown", onDocPointerDown, true);
6639 document.addEventListener("keydown", onDocKey);
6640 menu.addEventListener("tile-menu-closed", () => {
6641 document.removeEventListener(
6642 "pointerdown",
6643 onDocPointerDown,
6644 true
6645 );
6646 document.removeEventListener("keydown", onDocKey);
6647 });
6648 });
6649 }
6650 function closeAnyTileMenu() {
6651 document.querySelectorAll("wpd-context-menu.desktop-mode-my-wordpress__menu").forEach((n) => {
6652 n.dispatchEvent(new CustomEvent("tile-menu-closed"));
6653 n.remove();
6654 });
6655 }
6656 async function trashEntityById(entityId, id) {
6657 const cfg = getConfig();
6658 const entity = cfg.entities.find((e) => e.id === entityId);
6659 if (!entity) {
6660 throw new Error(
6661 sprintf(
6662 // translators: %s is the entity id (e.g. 'posts').
6663 __("Unknown My WordPress entity: %s", "desktop-mode"),
6664 entityId
6665 )
6666 );
6667 }
6668 await trashEntity(entity, id);
6669 document.dispatchEvent(
6670 new CustomEvent("desktop-mode-my-wordpress-entity-trashed", {
6671 detail: { entityId, id }
6672 })
6673 );
6674 }
6675 async function confirmTrash(state, ctx, entity, id, title) {
6676 const ok = await wpdConfirmGlobal({
6677 title: __("Move to Trash", "desktop-mode"),
6678 message: sprintf(
6679 // translators: %s is the entry title.
6680 __('Move "%s" to Trash?', "desktop-mode"),
6681 title
6682 ),
6683 confirmLabel: __("Move to Trash", "desktop-mode"),
6684 cancelLabel: __("Cancel", "desktop-mode"),
6685 danger: true
6686 });
6687 if (!ok) {
6688 return;
6689 }
6690 try {
6691 await trashEntity(entity, id);
6692 } catch (err) {
6693 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
6694 showToast(msg);
6695 return;
6696 }
6697 const tile = ctx.tiles.querySelector(
6698 `[data-entry-id="${id}"]`
6699 );
6700 tile?.remove();
6701 if (ctx.selectedId === id) {
6702 ctx.selectedId = null;
6703 ctx.selectedTile = null;
6704 ctx.preview.replaceChildren();
6705 const empty = document.createElement("div");
6706 empty.className = "desktop-mode-my-wordpress__preview-empty";
6707 empty.textContent = __(
6708 "Select an entry to preview it here.",
6709 "desktop-mode"
6710 );
6711 ctx.preview.appendChild(empty);
6712 }
6713 }
6714 function showToast(message) {
6715 const toast = window.wp?.desktop?.toast;
6716 if (typeof toast === "function") {
6717 toast({ message });
6718 return;
6719 }
6720 console.info("[my-wordpress]", message);
6721 }
6722 function renderUserEntityList(state, entity) {
6723 const cfg = getConfig();
6724 const initialQuery = lastQueryByEntity.get(entity.id) ?? "";
6725 const toolbar = renderListToolbar({
6726 placeholder: __("Search users…", "desktop-mode"),
6727 ariaLabel: __("Search users", "desktop-mode"),
6728 initialValue: initialQuery,
6729 onSearchChange: (q) => {
6730 lastQueryByEntity.set(entity.id, q);
6731 void resetForSearch(q);
6732 }
6733 });
6734 state.body.appendChild(toolbar.host);
6735 state.teardown.push(() => toolbar.destroy());
6736 const split = document.createElement("div");
6737 split.className = "desktop-mode-my-wordpress__split";
6738 const left = document.createElement("div");
6739 left.className = "desktop-mode-my-wordpress__list";
6740 const tiles = document.createElement("div");
6741 tiles.className = "desktop-mode-my-wordpress__tiles desktop-mode-my-wordpress__canvas desktop-mode-my-wordpress__canvas--users";
6742 tiles.setAttribute("role", "list");
6743 left.appendChild(tiles);
6744 const sentinel = document.createElement("div");
6745 sentinel.className = "desktop-mode-my-wordpress__sentinel";
6746 sentinel.setAttribute("aria-hidden", "true");
6747 left.appendChild(sentinel);
6748 const right = document.createElement("div");
6749 right.className = "desktop-mode-my-wordpress__preview";
6750 const previewEmpty = document.createElement("div");
6751 previewEmpty.className = "desktop-mode-my-wordpress__preview-empty";
6752 previewEmpty.textContent = __(
6753 "Select a user to see their profile here.",
6754 "desktop-mode"
6755 );
6756 right.appendChild(previewEmpty);
6757 split.appendChild(left);
6758 split.appendChild(right);
6759 state.body.appendChild(split);
6760 const tileLayout = createTileLayout(tiles, `entity:${entity.id}`);
6761 const menu = attachIconCanvasMenu(tiles, {
6762 scope: `my-wordpress:${entity.id}`,
6763 onSort: (mode) => tileLayout.sort(mode)
6764 });
6765 state.teardown.push(() => menu.dispose());
6766 const ctx = {
6767 page: 0,
6768 totalPages: 1,
6769 total: 0,
6770 loaded: 0,
6771 loading: false,
6772 done: false,
6773 tiles,
6774 sentinel,
6775 preview: right,
6776 selectedId: null,
6777 selectedTile: null,
6778 observer: null,
6779 layout: tileLayout,
6780 query: initialQuery,
6781 abort: null
6782 };
6783 state.teardown.push(() => tileLayout.dispose());
6784 state.teardown.push(() => ctx.abort?.abort());
6785 const repaintListStatus = () => {
6786 let itemLabel;
6787 if (ctx.total === 0 && ctx.loaded === 0) {
6788 itemLabel = pluralLabel(0, "user", "users");
6789 } else if (ctx.total > ctx.loaded && ctx.loaded > 0) {
6790 itemLabel = sprintf(
6791 // translators: 1: visible user count, 2: total user count.
6792 __("%1$d of %2$d users", "desktop-mode"),
6793 ctx.loaded,
6794 ctx.total
6795 );
6796 } else {
6797 itemLabel = pluralLabel(
6798 Math.max(ctx.total, ctx.loaded),
6799 "user",
6800 "users"
6801 );
6802 }
6803 const segments = [
6804 { id: "count", label: itemLabel, align: "start", sort: 10 }
6805 ];
6806 if (ctx.totalPages > 1) {
6807 segments.push({
6808 id: "page",
6809 label: sprintf(
6810 // translators: 1: current page, 2: total pages.
6811 __("Page %1$d of %2$d", "desktop-mode"),
6812 Math.max(ctx.page, 1),
6813 ctx.totalPages
6814 ),
6815 align: "end",
6816 sort: 10
6817 });
6818 }
6819 paintStatus(state, segments, {
6820 view: "list",
6821 entityId: entity.id
6822 });
6823 };
6824 repaintListStatus();
6825 const sentinelIsVisible = () => {
6826 const sr = sentinel.getBoundingClientRect();
6827 const rr = left.getBoundingClientRect();
6828 const slack = 200;
6829 return sr.top < rr.bottom + slack && sr.bottom > rr.top - slack;
6830 };
6831 const loadMore = async () => {
6832 if (ctx.loading || ctx.done) {
6833 return;
6834 }
6835 ctx.loading = true;
6836 const nextPage = ctx.page + 1;
6837 const isFirst = nextPage === 1;
6838 const queryAtFetchTime = ctx.query;
6839 showLoadingSkeleton(tiles, ctx.layout, isFirst);
6840 const controller = new AbortController();
6841 ctx.abort = controller;
6842 try {
6843 const result = await fetchUserList(entity, {
6844 page: nextPage,
6845 perPage: cfg.perPage,
6846 search: queryAtFetchTime || void 0,
6847 signal: controller.signal
6848 });
6849 if (ctx.query !== queryAtFetchTime) {
6850 return;
6851 }
6852 ctx.page = nextPage;
6853 ctx.totalPages = result.totalPages;
6854 ctx.total = result.total;
6855 hideLoadingSkeleton(tiles);
6856 if (result.items.length === 0 && isFirst) {
6857 renderListEmptyMessage(
6858 tiles,
6859 queryAtFetchTime ? sprintf(
6860 // translators: %s is the user-entered search query.
6861 __('No users match "%s".', "desktop-mode"),
6862 queryAtFetchTime
6863 ) : __("No users to show.", "desktop-mode")
6864 );
6865 ctx.done = true;
6866 repaintListStatus();
6867 return;
6868 }
6869 for (const item of result.items) {
6870 tiles.appendChild(
6871 buildUserTile(state, ctx, entity, item)
6872 );
6873 ctx.loaded += 1;
6874 }
6875 if (ctx.page >= ctx.totalPages) {
6876 ctx.done = true;
6877 }
6878 repaintListStatus();
6879 } catch (err) {
6880 if (isAbortError(err)) {
6881 return;
6882 }
6883 hideLoadingSkeleton(tiles);
6884 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
6885 renderListError(tiles, msg);
6886 ctx.done = true;
6887 } finally {
6888 ctx.loading = false;
6889 if (ctx.abort === controller) {
6890 ctx.abort = null;
6891 }
6892 }
6893 if (!ctx.done) {
6894 requestAnimationFrame(() => {
6895 if (sentinelIsVisible()) {
6896 void loadMore();
6897 }
6898 });
6899 }
6900 };
6901 const resetForSearch = async (q) => {
6902 ctx.abort?.abort();
6903 ctx.abort = null;
6904 ctx.query = q;
6905 tiles.classList.add(
6906 "desktop-mode-my-wordpress__tiles--searching"
6907 );
6908 hideLoadingSkeleton(tiles);
6909 const controller = new AbortController();
6910 ctx.abort = controller;
6911 ctx.loading = true;
6912 try {
6913 const result = await fetchUserList(entity, {
6914 page: 1,
6915 perPage: cfg.perPage,
6916 search: q || void 0,
6917 signal: controller.signal
6918 });
6919 if (ctx.query !== q) {
6920 return;
6921 }
6922 tiles.replaceChildren();
6923 ctx.layout.clear();
6924 tiles.classList.remove(
6925 "desktop-mode-my-wordpress__tiles--searching"
6926 );
6927 ctx.page = 1;
6928 ctx.totalPages = result.totalPages;
6929 ctx.total = result.total;
6930 ctx.loaded = 0;
6931 ctx.done = ctx.page >= ctx.totalPages;
6932 ctx.selectedId = null;
6933 ctx.selectedTile = null;
6934 ctx.preview.replaceChildren();
6935 const emptyPreview = document.createElement("div");
6936 emptyPreview.className = "desktop-mode-my-wordpress__preview-empty";
6937 emptyPreview.textContent = __(
6938 "Select a user to see their profile here.",
6939 "desktop-mode"
6940 );
6941 ctx.preview.appendChild(emptyPreview);
6942 if (result.items.length === 0) {
6943 renderListEmptyMessage(
6944 tiles,
6945 q ? sprintf(
6946 // translators: %s is the user-entered search query.
6947 __('No users match "%s".', "desktop-mode"),
6948 q
6949 ) : __("No users to show.", "desktop-mode")
6950 );
6951 ctx.done = true;
6952 } else {
6953 for (const item of result.items) {
6954 tiles.appendChild(
6955 buildUserTile(state, ctx, entity, item)
6956 );
6957 ctx.loaded += 1;
6958 }
6959 }
6960 repaintListStatus();
6961 } catch (err) {
6962 if (isAbortError(err)) {
6963 return;
6964 }
6965 tiles.classList.remove(
6966 "desktop-mode-my-wordpress__tiles--searching"
6967 );
6968 tiles.replaceChildren();
6969 ctx.layout.clear();
6970 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
6971 renderListError(tiles, msg);
6972 ctx.done = true;
6973 } finally {
6974 ctx.loading = false;
6975 if (ctx.abort === controller) {
6976 ctx.abort = null;
6977 }
6978 }
6979 if (!ctx.done) {
6980 requestAnimationFrame(() => {
6981 if (sentinelIsVisible()) {
6982 void loadMore();
6983 }
6984 });
6985 }
6986 };
6987 if (typeof IntersectionObserver !== "undefined") {
6988 ctx.observer = new IntersectionObserver(
6989 (entries) => {
6990 for (const e of entries) {
6991 if (e.isIntersecting) {
6992 void loadMore();
6993 }
6994 }
6995 },
6996 { root: left, rootMargin: "200px 0px" }
6997 );
6998 ctx.observer.observe(sentinel);
6999 state.teardown.push(() => ctx.observer?.disconnect());
7000 }
7001 void loadMore();
7002 }
7003 function buildUserTile(state, ctx, entity, item) {
7004 const displayName = item.name || item.slug || `#${item.id}`;
7005 const avatarUrl = pickAvatar(item.avatar_urls) ?? "";
7006 const tile = buildTileFromSpec({
7007 type: "user",
7008 ref: String(item.id),
7009 label: displayName,
7010 thumbnail: avatarUrl || void 0,
7011 // No avatar: fall back to a generic users dashicon so the
7012 // tile still has a visual. The initials block below
7013 // replaces that icon as a richer fallback.
7014 icon: avatarUrl ? void 0 : "dashicons-admin-users",
7015 role: "entry",
7016 dataset: { userId: item.id, role: "user" },
7017 extraClasses: [
7018 "desktop-mode-my-wordpress__tile",
7019 "desktop-mode-my-wordpress__tile--user"
7020 ]
7021 });
7022 if (!avatarUrl) {
7023 const iconHost = tile.querySelector(
7024 ".desktop-mode-file-tile__visual"
7025 );
7026 if (iconHost) {
7027 iconHost.replaceChildren();
7028 const initials = document.createElement("span");
7029 initials.className = "desktop-mode-my-wordpress__user-tile-initials";
7030 initials.textContent = initialsOf(displayName);
7031 iconHost.appendChild(initials);
7032 }
7033 }
7034 const summary = item.desktop_mode_summary;
7035 const postCount = summary?.postCount ?? 0;
7036 const roleLabel = (summary?.roleLabels ?? [])[0] ?? "";
7037 if (roleLabel || postCount > 0) {
7038 const sub = document.createElement("span");
7039 sub.className = "desktop-mode-my-wordpress__user-tile-sub";
7040 const parts = [];
7041 if (roleLabel) {
7042 parts.push(roleLabel);
7043 }
7044 if (postCount > 0) {
7045 parts.push(
7046 sprintf(
7047 // translators: %d is a count of posts authored.
7048 _n("%d post", "%d posts", postCount),
7049 postCount
7050 )
7051 );
7052 }
7053 sub.textContent = parts.join(" · ");
7054 tile.appendChild(sub);
7055 }
7056 const tooltip = buildUserTooltip(displayName, item);
7057 let tooltipNode = null;
7058 const showTooltip = (ev) => {
7059 if (!tooltipNode) {
7060 tooltipNode = tooltip;
7061 }
7062 document.body.appendChild(tooltipNode);
7063 positionTooltip(tooltipNode, ev);
7064 };
7065 const moveTooltip = (ev) => {
7066 if (tooltipNode && tooltipNode.isConnected) {
7067 positionTooltip(tooltipNode, ev);
7068 }
7069 };
7070 const hideTooltip = () => {
7071 if (tooltipNode && tooltipNode.isConnected) {
7072 tooltipNode.remove();
7073 }
7074 };
7075 tile.addEventListener("mouseenter", showTooltip);
7076 tile.addEventListener("mousemove", moveTooltip);
7077 tile.addEventListener("mouseleave", hideTooltip);
7078 state.teardown.push(hideTooltip);
7079 attachTileDragOut(
7080 tile,
7081 {
7082 kind: "user",
7083 ref: String(item.id),
7084 title: displayName,
7085 icon: "dashicons-admin-users",
7086 // Cross-frame bridge payload — receiver inserts a
7087 // `core/paragraph` with `<a href>` pointing at the
7088 // author archive (`item.link`). Falls back to empty
7089 // string when the REST shape omitted the link; the
7090 // receiver gates on a truthy URL.
7091 bridgePayload: {
7092 kind: "user",
7093 id: item.id,
7094 url: item.link ?? "",
7095 title: displayName
7096 }
7097 },
7098 () => hideTooltip()
7099 );
7100 const tileKey = `entry:${item.id}`;
7101 ctx.layout.place(tile, tileKey, {
7102 name: displayName,
7103 // Order users by post count by default — the most active
7104 // surface first. Authoring date isn't available per-user,
7105 // so we synthesize a date that ranks more-prolific users
7106 // earlier when the canvas sort-by-date is selected.
7107 date: postCount > 0 ? new Date(2100, 0, 1 - postCount).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString()
7108 });
7109 tile.addEventListener("click", () => {
7110 selectUserTile(state, ctx, tile, item);
7111 });
7112 tile.addEventListener("dblclick", (e) => {
7113 e.preventDefault();
7114 hideTooltip();
7115 navigate(state, {
7116 kind: "user-footprint",
7117 entityId: entity.id,
7118 userId: item.id,
7119 userName: displayName
7120 });
7121 });
7122 tile.addEventListener("contextmenu", (e) => {
7123 e.preventDefault();
7124 hideTooltip();
7125 openUserTileMenu(state, entity, item, displayName, {
7126 x: e.clientX,
7127 y: e.clientY
7128 });
7129 });
7130 return tile;
7131 }
7132 function buildUserTooltip(name, item) {
7133 const tip = document.createElement("div");
7134 tip.className = "desktop-mode-my-wordpress__tooltip";
7135 tip.setAttribute("role", "tooltip");
7136 const heading = document.createElement("div");
7137 heading.className = "desktop-mode-my-wordpress__tooltip-title";
7138 heading.textContent = name;
7139 tip.appendChild(heading);
7140 const summary = item.desktop_mode_summary;
7141 const roleLabel = (summary?.roleLabels ?? [])[0];
7142 const postCount = summary?.postCount ?? 0;
7143 const lastActive = summary?.lastActive ?? "";
7144 const lines = [];
7145 if (roleLabel) {
7146 lines.push(roleLabel);
7147 }
7148 if (postCount > 0) {
7149 lines.push(
7150 sprintf(
7151 // translators: %d is a count of posts authored by a user.
7152 _n("%d post", "%d posts", postCount),
7153 postCount
7154 )
7155 );
7156 }
7157 if (lastActive) {
7158 lines.push(
7159 sprintf(
7160 // translators: %s is a relative or absolute date.
7161 __("Last published %s", "desktop-mode"),
7162 formatDate(lastActive)
7163 )
7164 );
7165 }
7166 for (const ln of lines) {
7167 const p = document.createElement("p");
7168 p.className = "desktop-mode-my-wordpress__tooltip-excerpt";
7169 p.textContent = ln;
7170 tip.appendChild(p);
7171 }
7172 const bio = (item.description ?? "").trim();
7173 if (bio) {
7174 const p = document.createElement("p");
7175 p.className = "desktop-mode-my-wordpress__tooltip-excerpt";
7176 p.textContent = bio.length > 200 ? bio.slice(0, 197) + "" : bio;
7177 tip.appendChild(p);
7178 }
7179 return tip;
7180 }
7181 function selectUserTile(state, ctx, tile, item) {
7182 if (ctx.selectedTile) {
7183 ctx.selectedTile.classList.remove(
7184 "desktop-mode-file-tile--selected"
7185 );
7186 }
7187 tile.classList.add("desktop-mode-file-tile--selected");
7188 ctx.selectedTile = tile;
7189 ctx.selectedId = item.id;
7190 void renderUserPreviewPane(state, ctx, item);
7191 }
7192 async function renderUserPreviewPane(state, ctx, item) {
7193 const fallbackName = item.name || item.slug || `#${item.id}`;
7194 const fallbackAvatar = pickAvatar(item.avatar_urls) ?? "";
7195 const userId = item.id;
7196 showPreviewLoading(ctx.preview);
7197 let node;
7198 try {
7199 node = await renderUserDossier({
7200 userId,
7201 fallbackName,
7202 fallbackAvatar,
7203 fallbackDescription: item.description ?? ""
7204 });
7205 } catch (err) {
7206 if (ctx.selectedId !== userId) {
7207 return;
7208 }
7209 showPreviewError(ctx.preview, err);
7210 return;
7211 }
7212 if (ctx.selectedId !== userId) {
7213 return;
7214 }
7215 const footer = document.createElement("footer");
7216 footer.className = "desktop-mode-my-wordpress__article-footer";
7217 const footprintBtn = document.createElement("wpd-button");
7218 footprintBtn.setAttribute("variant", "primary");
7219 footprintBtn.textContent = __("View activity footprint", "desktop-mode");
7220 footprintBtn.title = __(
7221 "Open the full activity footprint surface for this user.",
7222 "desktop-mode"
7223 );
7224 footprintBtn.addEventListener("click", () => {
7225 navigate(state, {
7226 kind: "user-footprint",
7227 entityId: "users",
7228 userId,
7229 userName: fallbackName
7230 });
7231 });
7232 footer.appendChild(footprintBtn);
7233 const editBtn = document.createElement("wpd-button");
7234 editBtn.setAttribute("variant", "secondary");
7235 editBtn.textContent = __("Show profile", "desktop-mode");
7236 editBtn.title = __(
7237 "Open this user’s profile editor in a new window.",
7238 "desktop-mode"
7239 );
7240 editBtn.addEventListener("click", () => {
7241 openUserEditWindow(userId);
7242 });
7243 footer.appendChild(editBtn);
7244 node.appendChild(footer);
7245 ctx.preview.replaceChildren(node);
7246 }
7247 function openUserTileMenu(state, entity, item, name, pos) {
7248 closeAnyTileMenu();
7249 const menu = document.createElement("wpd-context-menu");
7250 menu.setAttribute("open", "");
7251 menu.classList.add("desktop-mode-my-wordpress__menu");
7252 menu.style.left = `${pos.x}px`;
7253 menu.style.top = `${pos.y}px`;
7254 const addOption = (id, label, icon) => {
7255 const opt = document.createElement("wpd-context-menu-option");
7256 opt.dataset.menuItemId = id;
7257 opt.setAttribute("value", id);
7258 opt.setAttribute("icon", sanitizeClass(icon));
7259 opt.textContent = label;
7260 menu.appendChild(opt);
7261 };
7262 addOption(
7263 "footprint",
7264 __("View activity footprint", "desktop-mode"),
7265 "dashicons-chart-area"
7266 );
7267 addOption(
7268 "open-profile",
7269 __("Show profile", "desktop-mode"),
7270 "dashicons-id-alt"
7271 );
7272 if (item.link) {
7273 addOption(
7274 "author-archive",
7275 __("View author archive", "desktop-mode"),
7276 "dashicons-external"
7277 );
7278 }
7279 menu.addEventListener("wpd-context-menu-pick", (e) => {
7280 const detail = e.detail;
7281 closeAnyTileMenu();
7282 if (detail.id === "footprint") {
7283 navigate(state, {
7284 kind: "user-footprint",
7285 entityId: entity.id,
7286 userId: item.id,
7287 userName: name
7288 });
7289 return;
7290 }
7291 if (detail.id === "open-profile") {
7292 openUserEditWindow(item.id);
7293 return;
7294 }
7295 if (detail.id === "author-archive" && item.link) {
7296 window.open(item.link, "_blank", "noopener,noreferrer");
7297 }
7298 });
7299 document.body.appendChild(menu);
7300 const rect = menu.getBoundingClientRect();
7301 if (rect.right > window.innerWidth) {
7302 menu.style.left = `${Math.max(
7303 0,
7304 window.innerWidth - rect.width - 8
7305 )}px`;
7306 }
7307 if (rect.bottom > window.innerHeight) {
7308 menu.style.top = `${Math.max(
7309 0,
7310 window.innerHeight - rect.height - 8
7311 )}px`;
7312 }
7313 queueMicrotask(() => {
7314 const onDocPointerDown = (ev) => {
7315 const target = ev.target;
7316 if (target instanceof Node && menu.contains(target)) {
7317 return;
7318 }
7319 closeAnyTileMenu();
7320 };
7321 const onDocKey = (ev) => {
7322 if (ev.key === "Escape") {
7323 closeAnyTileMenu();
7324 }
7325 };
7326 document.addEventListener("pointerdown", onDocPointerDown, true);
7327 document.addEventListener("keydown", onDocKey);
7328 menu.addEventListener("tile-menu-closed", () => {
7329 document.removeEventListener(
7330 "pointerdown",
7331 onDocPointerDown,
7332 true
7333 );
7334 document.removeEventListener("keydown", onDocKey);
7335 });
7336 });
7337 }
7338 function openUserEditWindow(userId) {
7339 if (!Number.isFinite(userId) || userId <= 0) {
7340 return;
7341 }
7342 const desktop = window.wp?.desktop;
7343 const createSharedStore = desktop?.createSharedStore;
7344 if (typeof createSharedStore === "function") {
7345 const store = createSharedStore(
7346 "desktop-mode/user-edit/target",
7347 () => ({ userId: null, requestedAt: 0, tabRequested: false })
7348 );
7349 store.state.userId = userId;
7350 store.state.requestedAt = Date.now();
7351 store.state.tabRequested = true;
7352 store.notify();
7353 }
7354 const opened = desktop?.openWindow?.("desktop-mode-user-edit", {
7355 source: "my-wordpress/user-tile"
7356 });
7357 if (!opened) {
7358 openIframeWindow({
7359 id: `user-edit-${userId}`,
7360 url: buildEditUserUrl(userId),
7361 title: __("Edit user", "desktop-mode"),
7362 icon: "dashicons-admin-users"
7363 });
7364 }
7365 }
7366 function initialsOf(name) {
7367 const parts = name.trim().split(/\s+/).filter((s) => s.length > 0);
7368 if (parts.length === 0) {
7369 return "?";
7370 }
7371 if (parts.length === 1) {
7372 return parts[0].slice(0, 2).toUpperCase();
7373 }
7374 return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
7375 }
7376 function renderUserFootprint(state, entity, userId, userName) {
7377 const host = document.createElement("div");
7378 host.className = "desktop-mode-my-wordpress__footprint";
7379 state.body.appendChild(host);
7380 showPreviewLoading(host);
7381 paintStatus(
7382 state,
7383 [
7384 {
7385 id: "loading",
7386 label: __("Loading footprint…", "desktop-mode"),
7387 align: "start",
7388 sort: 10
7389 }
7390 ],
7391 { view: "detail", entityId: entity.id, postId: userId }
7392 );
7393 void (async () => {
7394 let payload;
7395 try {
7396 payload = await fetchUserFootprint(userId);
7397 } catch (err) {
7398 showPreviewError(host, err);
7399 paintStatus(
7400 state,
7401 [
7402 {
7403 id: "error",
7404 label: __("Could not load footprint.", "desktop-mode"),
7405 align: "start",
7406 sort: 10
7407 }
7408 ],
7409 { view: "detail", entityId: entity.id, postId: userId }
7410 );
7411 return;
7412 }
7413 if (state.route.kind !== "user-footprint" || state.route.userId !== userId) {
7414 return;
7415 }
7416 host.replaceChildren();
7417 host.appendChild(buildFootprintHero(payload));
7418 host.appendChild(buildFootprintHeadlineStats(payload));
7419 host.appendChild(buildFootprintCalendar(payload));
7420 host.appendChild(buildFootprintRhythm(payload));
7421 const monthCallout = buildFootprintMonthCallout(payload);
7422 if (monthCallout) {
7423 host.appendChild(monthCallout);
7424 }
7425 host.appendChild(buildFootprintTimeline(payload));
7426 host.appendChild(
7427 buildFootprintFooter(payload, userId)
7428 );
7429 paintStatus(
7430 state,
7431 [
7432 {
7433 id: "count",
7434 label: sprintf(
7435 // translators: 1: post total, 2: comment total.
7436 __(
7437 "%1$d posts · %2$d comments tracked",
7438 "desktop-mode"
7439 ),
7440 payload.totals.posts + payload.totals.pages,
7441 payload.totals.comments
7442 ),
7443 align: "start",
7444 sort: 10
7445 },
7446 {
7447 id: "range",
7448 label: sprintf(
7449 // translators: 1: window-start date, 2: window-end date.
7450 __(
7451 "Window %1$s → %2$s",
7452 "desktop-mode"
7453 ),
7454 formatShortDate(payload.range.from),
7455 formatShortDate(payload.range.to)
7456 ),
7457 align: "end",
7458 sort: 10
7459 }
7460 ],
7461 { view: "detail", entityId: entity.id, postId: userId }
7462 );
7463 })();
7464 }
7465 function buildFootprintHero(payload) {
7466 const hero = document.createElement("header");
7467 hero.className = "desktop-mode-my-wordpress__footprint-hero";
7468 const avatar = document.createElement("div");
7469 avatar.className = "desktop-mode-my-wordpress__footprint-avatar";
7470 if (payload.profile.avatarUrl) {
7471 const img = document.createElement("img");
7472 img.src = payload.profile.avatarUrl;
7473 img.alt = "";
7474 avatar.appendChild(img);
7475 } else {
7476 const span = document.createElement("span");
7477 span.className = "desktop-mode-my-wordpress__user-tile-initials";
7478 span.textContent = initialsOf(payload.profile.name);
7479 avatar.appendChild(span);
7480 }
7481 hero.appendChild(avatar);
7482 const text = document.createElement("div");
7483 text.className = "desktop-mode-my-wordpress__footprint-headline";
7484 const h = document.createElement("h1");
7485 h.className = "desktop-mode-my-wordpress__footprint-title";
7486 h.textContent = payload.profile.name;
7487 text.appendChild(h);
7488 const meta = document.createElement("div");
7489 meta.className = "desktop-mode-my-wordpress__footprint-meta";
7490 const roles = payload.profile.roleLabels ?? [];
7491 for (const r of roles) {
7492 const chip = document.createElement("span");
7493 chip.className = "desktop-mode-my-wordpress__user-role";
7494 chip.textContent = r;
7495 meta.appendChild(chip);
7496 }
7497 if (payload.profile.registered) {
7498 const since = document.createElement("span");
7499 since.className = "desktop-mode-my-wordpress__user-role desktop-mode-my-wordpress__footprint-since";
7500 since.textContent = sprintf(
7501 // translators: %s is a year-month label like "January 2023".
7502 __("Member since %s", "desktop-mode"),
7503 formatYearMonth(payload.profile.registered)
7504 );
7505 meta.appendChild(since);
7506 }
7507 text.appendChild(meta);
7508 if (payload.profile.link) {
7509 const links = document.createElement("div");
7510 links.className = "desktop-mode-my-wordpress__user-links";
7511 const a = document.createElement("a");
7512 a.href = payload.profile.link;
7513 a.target = "_blank";
7514 a.rel = "noopener noreferrer";
7515 a.textContent = __("Author archive", "desktop-mode");
7516 links.appendChild(a);
7517 text.appendChild(links);
7518 }
7519 hero.appendChild(text);
7520 return hero;
7521 }
7522 function buildFootprintHeadlineStats(payload) {
7523 const wrap = document.createElement("section");
7524 wrap.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-stats-row";
7525 const totalContent = payload.totals.posts + payload.totals.pages;
7526 wrap.appendChild(
7527 buildStatCard(
7528 totalContent.toLocaleString(),
7529 __("Total content", "desktop-mode"),
7530 payload.totals.posts > 0 && payload.totals.pages > 0 ? sprintf(
7531 // translators: 1: post count, 2: page count.
7532 __(
7533 "%1$d posts · %2$d pages",
7534 "desktop-mode"
7535 ),
7536 payload.totals.posts,
7537 payload.totals.pages
7538 ) : ""
7539 )
7540 );
7541 wrap.appendChild(
7542 buildStatCard(
7543 payload.totals.comments.toLocaleString(),
7544 __("Comments left", "desktop-mode"),
7545 ""
7546 )
7547 );
7548 const updateCount = payload.totals.updates ?? 0;
7549 if (updateCount > 0) {
7550 wrap.appendChild(
7551 buildStatCard(
7552 updateCount.toLocaleString(),
7553 __("Updates", "desktop-mode"),
7554 __("Saves on existing posts", "desktop-mode")
7555 )
7556 );
7557 }
7558 const longestRange = payload.streak.longestRange;
7559 const longestCaption = longestRange.from && longestRange.to ? sprintf(
7560 // translators: 1: start date, 2: end date.
7561 __("%1$s → %2$s", "desktop-mode"),
7562 formatShortDate(longestRange.from),
7563 formatShortDate(longestRange.to)
7564 ) : "";
7565 wrap.appendChild(
7566 buildStatCard(
7567 sprintf(
7568 // translators: %d is the length in days of the user's longest publishing streak.
7569 _n(
7570 "%d day",
7571 "%d days",
7572 payload.streak.longest
7573 ),
7574 payload.streak.longest
7575 ),
7576 __("Longest streak", "desktop-mode"),
7577 longestCaption
7578 )
7579 );
7580 wrap.appendChild(
7581 buildStatCard(
7582 sprintf(
7583 // translators: %d is the length in days of the user's current active streak.
7584 _n("%d day", "%d days", payload.streak.current),
7585 payload.streak.current
7586 ),
7587 __("Current streak", "desktop-mode"),
7588 payload.streak.current === 0 ? __("No activity today", "desktop-mode") : __("Including today", "desktop-mode")
7589 )
7590 );
7591 return wrap;
7592 }
7593 function buildFootprintCalendar(payload) {
7594 const section = document.createElement("section");
7595 section.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-calendar-section";
7596 const h = document.createElement("h3");
7597 h.textContent = __("A year of activity", "desktop-mode");
7598 section.appendChild(h);
7599 const calendar = document.createElement("div");
7600 calendar.className = "desktop-mode-my-wordpress__footprint-calendar";
7601 const dayIntensity = (d) => d.posts + d.comments + (d.updates ?? 0);
7602 const maxIntensity = payload.daily.reduce((m, d) => {
7603 const v = dayIntensity(d);
7604 return v > m ? v : m;
7605 }, 0);
7606 const bucketize = (v) => {
7607 if (v <= 0) {
7608 return 0;
7609 }
7610 if (maxIntensity <= 0) {
7611 return 0;
7612 }
7613 const ratio = v / maxIntensity;
7614 if (ratio > 0.75) {
7615 return 4;
7616 }
7617 if (ratio > 0.5) {
7618 return 3;
7619 }
7620 if (ratio > 0.25) {
7621 return 2;
7622 }
7623 return 1;
7624 };
7625 const dates = payload.daily.map((d) => /* @__PURE__ */ new Date(d.date + "T00:00:00Z"));
7626 if (dates.length === 0) {
7627 const empty = document.createElement("p");
7628 empty.className = "desktop-mode-my-wordpress__article-meta";
7629 empty.textContent = __(
7630 "No activity recorded in the last year.",
7631 "desktop-mode"
7632 );
7633 section.appendChild(empty);
7634 return section;
7635 }
7636 const firstDow = dates[0].getUTCDay();
7637 const grid = document.createElement("div");
7638 grid.className = "desktop-mode-my-wordpress__footprint-grid";
7639 const placeCell = (el, linearDayOffset) => {
7640 const dow = linearDayOffset % 7;
7641 const week = Math.floor(linearDayOffset / 7);
7642 el.style.gridRow = String(dow + 2);
7643 el.style.gridColumn = String(week + 2);
7644 };
7645 const weekdaySource = [
7646 // 2024-12-02 was a Monday (UTC).
7647 new Date(Date.UTC(2024, 11, 2)),
7648 // Mon
7649 new Date(Date.UTC(2024, 11, 4)),
7650 // Wed
7651 new Date(Date.UTC(2024, 11, 6))
7652 // Fri
7653 ];
7654 const weekdayRows = [2, 4, 6];
7655 for (let i = 0; i < weekdaySource.length; i += 1) {
7656 const lbl = document.createElement("span");
7657 lbl.className = "desktop-mode-my-wordpress__footprint-weekday";
7658 lbl.textContent = weekdaySource[i].toLocaleDateString(void 0, {
7659 weekday: "short"
7660 });
7661 lbl.style.gridColumn = "1";
7662 lbl.style.gridRow = String(weekdayRows[i] + 1);
7663 grid.appendChild(lbl);
7664 }
7665 for (let i = 0; i < firstDow; i += 1) {
7666 const blank = document.createElement("span");
7667 blank.className = "desktop-mode-my-wordpress__footprint-cell desktop-mode-my-wordpress__footprint-cell--pad";
7668 blank.setAttribute("aria-hidden", "true");
7669 placeCell(blank, i);
7670 grid.appendChild(blank);
7671 }
7672 let lastMonth = -1;
7673 for (let i = 0; i < payload.daily.length; i += 1) {
7674 const d = dates[i];
7675 const m = d.getUTCMonth();
7676 if (m === lastMonth) {
7677 continue;
7678 }
7679 lastMonth = m;
7680 const linear = firstDow + i;
7681 const week = Math.floor(linear / 7);
7682 if (week === 0 && linear % 7 !== 0) {
7683 continue;
7684 }
7685 const lbl = document.createElement("span");
7686 lbl.className = "desktop-mode-my-wordpress__footprint-month";
7687 lbl.textContent = d.toLocaleDateString(void 0, { month: "short" });
7688 lbl.style.gridRow = "1";
7689 lbl.style.gridColumn = String(week + 2);
7690 grid.appendChild(lbl);
7691 }
7692 for (let i = 0; i < payload.daily.length; i += 1) {
7693 const d = payload.daily[i];
7694 const intensity = bucketize(dayIntensity(d));
7695 const cell = document.createElement("span");
7696 cell.className = `desktop-mode-my-wordpress__footprint-cell desktop-mode-my-wordpress__footprint-cell--l${intensity}`;
7697 cell.title = sprintf(
7698 // translators: 1: date, 2: post count, 3: comment count, 4: update (re-save) count.
7699 __(
7700 "%1$s — %2$d posts, %3$d comments, %4$d updates",
7701 "desktop-mode"
7702 ),
7703 formatLongDate(d.date),
7704 d.posts,
7705 d.comments,
7706 d.updates ?? 0
7707 );
7708 cell.dataset.date = d.date;
7709 placeCell(cell, firstDow + i);
7710 grid.appendChild(cell);
7711 }
7712 calendar.appendChild(grid);
7713 const legend = document.createElement("div");
7714 legend.className = "desktop-mode-my-wordpress__footprint-legend";
7715 const less = document.createElement("span");
7716 less.className = "desktop-mode-my-wordpress__footprint-legend-label";
7717 less.textContent = __("Less", "desktop-mode");
7718 legend.appendChild(less);
7719 for (let i = 0; i <= 4; i += 1) {
7720 const sw = document.createElement("span");
7721 sw.className = `desktop-mode-my-wordpress__footprint-cell desktop-mode-my-wordpress__footprint-cell--l${i}`;
7722 legend.appendChild(sw);
7723 }
7724 const more = document.createElement("span");
7725 more.className = "desktop-mode-my-wordpress__footprint-legend-label";
7726 more.textContent = __("More", "desktop-mode");
7727 legend.appendChild(more);
7728 calendar.appendChild(legend);
7729 section.appendChild(calendar);
7730 return section;
7731 }
7732 function buildFootprintRhythm(payload) {
7733 const section = document.createElement("section");
7734 section.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-rhythm";
7735 const h = document.createElement("h3");
7736 h.textContent = __("Publishing rhythm", "desktop-mode");
7737 section.appendChild(h);
7738 const grid = document.createElement("div");
7739 grid.className = "desktop-mode-my-wordpress__footprint-rhythm-grid";
7740 const weekdayWrap = document.createElement("div");
7741 weekdayWrap.className = "desktop-mode-my-wordpress__footprint-chart";
7742 const weekdayCap = document.createElement("div");
7743 weekdayCap.className = "desktop-mode-my-wordpress__footprint-chart-caption";
7744 weekdayCap.textContent = __("By weekday", "desktop-mode");
7745 weekdayWrap.appendChild(weekdayCap);
7746 const weekdayLabels = [
7747 __("S", "desktop-mode"),
7748 __("M", "desktop-mode"),
7749 __("T", "desktop-mode"),
7750 __("W", "desktop-mode"),
7751 __("T", "desktop-mode"),
7752 __("F", "desktop-mode"),
7753 __("S", "desktop-mode")
7754 ];
7755 const weekdayFull = [
7756 __("Sunday", "desktop-mode"),
7757 __("Monday", "desktop-mode"),
7758 __("Tuesday", "desktop-mode"),
7759 __("Wednesday", "desktop-mode"),
7760 __("Thursday", "desktop-mode"),
7761 __("Friday", "desktop-mode"),
7762 __("Saturday", "desktop-mode")
7763 ];
7764 weekdayWrap.appendChild(
7765 buildBarChart(payload.weekday, weekdayLabels, weekdayFull)
7766 );
7767 grid.appendChild(weekdayWrap);
7768 const hourWrap = document.createElement("div");
7769 hourWrap.className = "desktop-mode-my-wordpress__footprint-chart";
7770 const hourCap = document.createElement("div");
7771 hourCap.className = "desktop-mode-my-wordpress__footprint-chart-caption";
7772 hourCap.textContent = __("By hour of day (site time)", "desktop-mode");
7773 hourWrap.appendChild(hourCap);
7774 const hourLabels = [
7775 "0",
7776 "",
7777 "",
7778 "3",
7779 "",
7780 "",
7781 "6",
7782 "",
7783 "",
7784 "9",
7785 "",
7786 "",
7787 "12",
7788 "",
7789 "",
7790 "15",
7791 "",
7792 "",
7793 "18",
7794 "",
7795 "",
7796 "21",
7797 "",
7798 ""
7799 ];
7800 const hourFull = Array.from(
7801 { length: 24 },
7802 (_, i) => sprintf(
7803 // translators: %d is an hour of the day (0-23).
7804 __("%d:00", "desktop-mode"),
7805 i
7806 )
7807 );
7808 hourWrap.appendChild(
7809 buildBarChart(payload.hour, hourLabels, hourFull)
7810 );
7811 grid.appendChild(hourWrap);
7812 section.appendChild(grid);
7813 return section;
7814 }
7815 function buildBarChart(values, labels, titles) {
7816 const chart = document.createElement("div");
7817 chart.className = "desktop-mode-my-wordpress__footprint-bars";
7818 const max = Math.max(1, ...values);
7819 values.forEach((v, i) => {
7820 const col = document.createElement("div");
7821 col.className = "desktop-mode-my-wordpress__footprint-bar-col";
7822 const bar = document.createElement("div");
7823 bar.className = "desktop-mode-my-wordpress__footprint-bar";
7824 bar.style.height = `${Math.round(v / max * 100)}%`;
7825 bar.title = sprintf(
7826 // translators: 1: bucket label, 2: count.
7827 __(
7828 "%1$s · %2$d",
7829 "desktop-mode"
7830 ),
7831 titles[i] ?? labels[i] ?? String(i),
7832 v
7833 );
7834 if (v === 0) {
7835 bar.classList.add(
7836 "desktop-mode-my-wordpress__footprint-bar--empty"
7837 );
7838 }
7839 col.appendChild(bar);
7840 const lbl = document.createElement("span");
7841 lbl.className = "desktop-mode-my-wordpress__footprint-bar-label";
7842 lbl.textContent = labels[i] ?? "";
7843 col.appendChild(lbl);
7844 chart.appendChild(col);
7845 });
7846 return chart;
7847 }
7848 function buildFootprintMonthCallout(payload) {
7849 const m = payload.totals.mostProlificMonth;
7850 if (!m) {
7851 return null;
7852 }
7853 const section = document.createElement("section");
7854 section.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-callout";
7855 const label = document.createElement("span");
7856 label.className = "desktop-mode-my-wordpress__footprint-callout-label";
7857 label.textContent = __("Most prolific month", "desktop-mode");
7858 section.appendChild(label);
7859 const value = document.createElement("h3");
7860 value.className = "desktop-mode-my-wordpress__footprint-callout-value";
7861 value.textContent = formatYearMonth(m.ym + "-01T00:00:00Z");
7862 section.appendChild(value);
7863 const detail = document.createElement("p");
7864 detail.className = "desktop-mode-my-wordpress__footprint-callout-detail";
7865 detail.textContent = sprintf(
7866 // translators: %d is a post count.
7867 _n(
7868 "%d post published — their personal record.",
7869 "%d posts published — their personal record.",
7870 m.n
7871 ),
7872 m.n
7873 );
7874 section.appendChild(detail);
7875 return section;
7876 }
7877 function buildFootprintTimeline(payload) {
7878 const section = document.createElement("section");
7879 section.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-timeline-section";
7880 const h = document.createElement("h3");
7881 h.textContent = __("Recent activity", "desktop-mode");
7882 section.appendChild(h);
7883 if (payload.timeline.length === 0) {
7884 const empty = document.createElement("p");
7885 empty.className = "desktop-mode-my-wordpress__article-meta";
7886 empty.textContent = __("Nothing to show yet.", "desktop-mode");
7887 section.appendChild(empty);
7888 return section;
7889 }
7890 const list = document.createElement("ul");
7891 list.className = "desktop-mode-my-wordpress__footprint-timeline";
7892 for (const ev of payload.timeline) {
7893 const li = document.createElement("li");
7894 li.className = `desktop-mode-my-wordpress__footprint-event desktop-mode-my-wordpress__footprint-event--${ev.kind}`;
7895 const dot = document.createElement("span");
7896 dot.className = "desktop-mode-my-wordpress__footprint-dot";
7897 const icon = document.createElement("span");
7898 let iconClass = "dashicons-admin-post";
7899 if (ev.kind === "comment") {
7900 iconClass = "dashicons-admin-comments";
7901 } else if (ev.kind === "post-update") {
7902 iconClass = "dashicons-edit";
7903 }
7904 icon.className = "dashicons " + iconClass;
7905 icon.setAttribute("aria-hidden", "true");
7906 dot.appendChild(icon);
7907 li.appendChild(dot);
7908 const body = document.createElement("div");
7909 body.className = "desktop-mode-my-wordpress__footprint-event-body";
7910 const title = ev.title || __("(no title)", "desktop-mode");
7911 const titleNode = ev.link ? document.createElement("a") : document.createElement("span");
7912 titleNode.className = "desktop-mode-my-wordpress__footprint-event-title";
7913 if (ev.kind === "comment") {
7914 titleNode.textContent = sprintf(
7915 // translators: %s is a post title the user commented on.
7916 __("Commented on “%s”", "desktop-mode"),
7917 title
7918 );
7919 } else if (ev.kind === "post-update") {
7920 titleNode.textContent = sprintf(
7921 // translators: %s is the post title the user re-saved.
7922 __("Updated “%s”", "desktop-mode"),
7923 title
7924 );
7925 } else {
7926 titleNode.textContent = title;
7927 }
7928 if (ev.link && titleNode instanceof HTMLAnchorElement) {
7929 titleNode.href = ev.link;
7930 titleNode.target = "_blank";
7931 titleNode.rel = "noopener noreferrer";
7932 }
7933 body.appendChild(titleNode);
7934 const meta = document.createElement("span");
7935 meta.className = "desktop-mode-my-wordpress__footprint-event-meta";
7936 const parts = [formatLongDate(ev.date)];
7937 if (ev.status && ev.status !== "publish" && ev.status !== "approved") {
7938 parts.push(ev.status);
7939 }
7940 meta.textContent = parts.join(" · ");
7941 body.appendChild(meta);
7942 li.appendChild(body);
7943 list.appendChild(li);
7944 }
7945 section.appendChild(list);
7946 return section;
7947 }
7948 function buildFootprintFooter(payload, userId, userName) {
7949 const footer = document.createElement("footer");
7950 footer.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-footer";
7951 const archiveBtn = document.createElement("wpd-button");
7952 archiveBtn.setAttribute("variant", "ghost");
7953 archiveBtn.textContent = __("View author archive", "desktop-mode");
7954 archiveBtn.addEventListener("click", () => {
7955 if (payload.profile.link) {
7956 window.open(payload.profile.link, "_blank", "noopener,noreferrer");
7957 }
7958 });
7959 if (!payload.profile.link) {
7960 archiveBtn.setAttribute("disabled", "");
7961 }
7962 footer.appendChild(archiveBtn);
7963 const editBtn = document.createElement("wpd-button");
7964 editBtn.setAttribute("variant", "primary");
7965 editBtn.textContent = __("Show profile", "desktop-mode");
7966 editBtn.addEventListener("click", () => {
7967 openUserEditWindow(userId);
7968 });
7969 footer.appendChild(editBtn);
7970 return footer;
7971 }
7972 function formatShortDate(iso) {
7973 if (!iso) {
7974 return "";
7975 }
7976 try {
7977 return new Date(iso).toLocaleDateString(void 0, {
7978 month: "short",
7979 day: "numeric"
7980 });
7981 } catch {
7982 return iso;
7983 }
7984 }
7985 function formatLongDate(iso) {
7986 if (!iso) {
7987 return "";
7988 }
7989 try {
7990 return new Date(iso).toLocaleDateString(void 0, {
7991 year: "numeric",
7992 month: "short",
7993 day: "numeric"
7994 });
7995 } catch {
7996 return iso;
7997 }
7998 }
7999 function sanitizeClass(raw) {
8000 return (raw || "").replace(/[^a-zA-Z0-9_-]/g, "");
8001 }
8002 function extractContentMediaIds(html2) {
8003 if (!html2 || typeof html2 !== "string") {
8004 return [];
8005 }
8006 const ids = [];
8007 const seen = /* @__PURE__ */ new Set();
8008 const push = (raw) => {
8009 const id = parseInt(raw, 10);
8010 if (Number.isFinite(id) && id > 0 && !seen.has(id)) {
8011 seen.add(id);
8012 ids.push(id);
8013 }
8014 };
8015 const wpImage = /\bwp-image-(\d+)\b/g;
8016 let m;
8017 while ((m = wpImage.exec(html2)) !== null) {
8018 push(m[1]);
8019 }
8020 const captionShort = /\[caption[^\]]*id="attachment_(\d+)"/g;
8021 while ((m = captionShort.exec(html2)) !== null) {
8022 push(m[1]);
8023 }
8024 return ids;
8025 }
8026 function createTileSelector() {
8027 let selected = null;
8028 return (tile) => {
8029 if (selected === tile) {
8030 return;
8031 }
8032 if (selected) {
8033 selected.classList.remove(
8034 "desktop-mode-file-tile--selected"
8035 );
8036 }
8037 tile.classList.add("desktop-mode-file-tile--selected");
8038 selected = tile;
8039 };
8040 }
8041 const TILE_W = 108;
8042 const TILE_H = 112;
8043 const TILE_PAD = 16;
8044 function createTileLayout(host, scope) {
8045 const positions = loadPositions(scope);
8046 const entries = [];
8047 const occupied = /* @__PURE__ */ new Set();
8048 host.classList.add("desktop-mode-my-wordpress__canvas--positioned");
8049 const cellOf = (x, y) => ({
8050 col: Math.max(0, Math.round((x - TILE_PAD) / TILE_W)),
8051 row: Math.max(0, Math.round((y - TILE_PAD) / TILE_H))
8052 });
8053 const occupyAt = (x, y) => {
8054 const { col, row } = cellOf(x, y);
8055 occupied.add(`${col},${row}`);
8056 };
8057 const releaseAt = (x, y) => {
8058 const { col, row } = cellOf(x, y);
8059 occupied.delete(`${col},${row}`);
8060 };
8061 const recomputeHostHeight = () => {
8062 let maxBottom = 0;
8063 for (const child of Array.from(host.children)) {
8064 if (!(child instanceof HTMLElement)) {
8065 continue;
8066 }
8067 if (!child.classList.contains("desktop-mode-file-tile")) {
8068 continue;
8069 }
8070 const top = parseFloat(child.style.top || "0");
8071 maxBottom = Math.max(maxBottom, top + TILE_H);
8072 }
8073 host.style.minHeight = `${Math.max(0, maxBottom + TILE_PAD)}px`;
8074 };
8075 const nextFreeCell = (cols) => {
8076 for (let n = 0; ; n += 1) {
8077 const col = n % cols;
8078 const row = Math.floor(n / cols);
8079 if (!occupied.has(`${col},${row}`)) {
8080 return { col, row };
8081 }
8082 }
8083 };
8084 const place = (tile, key, sortable) => {
8085 const saved = positions[key];
8086 const width = host.clientWidth > 0 ? host.clientWidth : TILE_PAD + 5 * TILE_W;
8087 const cols = Math.max(
8088 1,
8089 Math.floor((width - TILE_PAD) / TILE_W)
8090 );
8091 const fits = saved && saved.x + TILE_W <= width;
8092 const entry = {
8093 key,
8094 tile,
8095 sortable,
8096 userPlaced: !!fits
8097 };
8098 entries.push(entry);
8099 let x;
8100 let y;
8101 if (fits && saved) {
8102 x = saved.x;
8103 y = saved.y;
8104 } else {
8105 if (saved && !fits) {
8106 delete positions[key];
8107 savePositions(scope, positions);
8108 }
8109 const cell = nextFreeCell(cols);
8110 x = TILE_PAD + cell.col * TILE_W;
8111 y = TILE_PAD + cell.row * TILE_H;
8112 }
8113 occupyAt(x, y);
8114 applyTilePosition(tile, x, y);
8115 recomputeHostHeight();
8116 };
8117 const commit = (tile, key, x, y) => {
8118 const oldX = parseFloat(tile.style.left || "0");
8119 const oldY = parseFloat(tile.style.top || "0");
8120 releaseAt(oldX, oldY);
8121 applyTilePosition(tile, x, y);
8122 occupyAt(x, y);
8123 positions[key] = { x, y };
8124 savePositions(scope, positions);
8125 const entry = entries.find((e) => e.key === key);
8126 if (entry) {
8127 entry.userPlaced = true;
8128 }
8129 recomputeHostHeight();
8130 };
8131 const reflow = () => {
8132 const width = host.clientWidth > 0 ? host.clientWidth : TILE_PAD + 5 * TILE_W;
8133 const cols = Math.max(
8134 1,
8135 Math.floor((width - TILE_PAD) / TILE_W)
8136 );
8137 const overflowing = entries.some((entry) => {
8138 const left = parseFloat(entry.tile.style.left || "0");
8139 return left + TILE_W > width;
8140 });
8141 if (overflowing) {
8142 for (const k of Object.keys(positions)) {
8143 delete positions[k];
8144 }
8145 savePositions(scope, positions);
8146 for (const entry of entries) {
8147 entry.userPlaced = false;
8148 }
8149 }
8150 occupied.clear();
8151 for (const entry of entries) {
8152 if (!entry.userPlaced) {
8153 continue;
8154 }
8155 const left = parseFloat(entry.tile.style.left || "0");
8156 const top = parseFloat(entry.tile.style.top || "0");
8157 occupyAt(left, top);
8158 }
8159 let autoCount = 0;
8160 for (const entry of entries) {
8161 if (entry.userPlaced) {
8162 continue;
8163 }
8164 const cell = nextFreeCell(cols);
8165 const x = TILE_PAD + cell.col * TILE_W;
8166 const y = TILE_PAD + cell.row * TILE_H;
8167 applyTilePosition(entry.tile, x, y);
8168 occupyAt(x, y);
8169 autoCount += 1;
8170 }
8171 recomputeHostHeight();
8172 doAction("desktop-mode.icon-canvas.reflow", {
8173 scope,
8174 cols,
8175 autoCount,
8176 overflowing
8177 });
8178 };
8179 const sort = (mode) => {
8180 const sorted = entries.slice().sort((a, b) => {
8181 switch (mode) {
8182 case "name-asc":
8183 return a.sortable.name.localeCompare(b.sortable.name);
8184 case "name-desc":
8185 return b.sortable.name.localeCompare(a.sortable.name);
8186 case "date-asc":
8187 return Date.parse(a.sortable.date) - Date.parse(b.sortable.date);
8188 case "date-desc":
8189 return Date.parse(b.sortable.date) - Date.parse(a.sortable.date);
8190 default:
8191 return 0;
8192 }
8193 });
8194 const width = host.clientWidth > 0 ? host.clientWidth : TILE_PAD + 5 * TILE_W;
8195 const cols = Math.max(
8196 1,
8197 Math.floor((width - TILE_PAD) / TILE_W)
8198 );
8199 for (const k of Object.keys(positions)) {
8200 delete positions[k];
8201 }
8202 occupied.clear();
8203 sorted.forEach((entry, idx) => {
8204 const col = idx % cols;
8205 const row = Math.floor(idx / cols);
8206 const x = TILE_PAD + col * TILE_W;
8207 const y = TILE_PAD + row * TILE_H;
8208 applyTilePosition(entry.tile, x, y);
8209 occupyAt(x, y);
8210 positions[entry.key] = { x, y };
8211 entry.userPlaced = true;
8212 });
8213 savePositions(scope, positions);
8214 for (const entry of sorted) {
8215 host.appendChild(entry.tile);
8216 }
8217 recomputeHostHeight();
8218 };
8219 let lastWidth = host.clientWidth;
8220 let resizeObserver = null;
8221 if (typeof ResizeObserver !== "undefined") {
8222 resizeObserver = new ResizeObserver(() => {
8223 const w = host.clientWidth;
8224 if (w === lastWidth) {
8225 return;
8226 }
8227 lastWidth = w;
8228 reflow();
8229 });
8230 resizeObserver.observe(host);
8231 }
8232 const peekNextCells = (count) => {
8233 const width = host.clientWidth > 0 ? host.clientWidth : TILE_PAD + 5 * TILE_W;
8234 const cols = Math.max(
8235 1,
8236 Math.floor((width - TILE_PAD) / TILE_W)
8237 );
8238 const taken = new Set(occupied);
8239 const out = [];
8240 for (let i = 0; i < count; i += 1) {
8241 for (let n = 0; ; n += 1) {
8242 const col = n % cols;
8243 const row = Math.floor(n / cols);
8244 const key = `${col},${row}`;
8245 if (taken.has(key)) {
8246 continue;
8247 }
8248 taken.add(key);
8249 out.push({
8250 x: TILE_PAD + col * TILE_W,
8251 y: TILE_PAD + row * TILE_H
8252 });
8253 break;
8254 }
8255 }
8256 return out;
8257 };
8258 const clear = () => {
8259 entries.length = 0;
8260 occupied.clear();
8261 host.style.minHeight = "";
8262 };
8263 return {
8264 host,
8265 scope,
8266 place,
8267 commit,
8268 sort,
8269 reflow,
8270 peekNextCells,
8271 clear,
8272 dispose: () => {
8273 resizeObserver?.disconnect();
8274 resizeObserver = null;
8275 }
8276 };
8277 }
8278 function applyTilePosition(tile, x, y) {
8279 tile.style.left = `${Math.round(x)}px`;
8280 tile.style.top = `${Math.round(y)}px`;
8281 }
8282 function loadPositions(scope) {
8283 try {
8284 const raw = window.localStorage.getItem(storageKey(scope));
8285 if (!raw) {
8286 return {};
8287 }
8288 const parsed = JSON.parse(raw);
8289 return parsed && typeof parsed === "object" ? parsed : {};
8290 } catch {
8291 return {};
8292 }
8293 }
8294 function savePositions(scope, positions) {
8295 try {
8296 window.localStorage.setItem(
8297 storageKey(scope),
8298 JSON.stringify(positions)
8299 );
8300 } catch {
8301 }
8302 }
8303 function storageKey(scope) {
8304 return `desktop-mode-my-wordpress:positions:${scope}`;
8305 }
8306 let activeState = null;
8307 const liveStates = /* @__PURE__ */ new Map();
8308 let pendingRoute = null;
8309 let rejectIdCounter = 0;
8310 function renderInto(body) {
8311 const root = body.querySelector(ROOT_SEL);
8312 if (!root) {
8313 return void 0;
8314 }
8315 const breadcrumbsHost = root.querySelector(BREADCRUMBS_SEL);
8316 const bodyHost = root.querySelector(BODY_SEL);
8317 const statusHost = root.querySelector(STATUS_SEL);
8318 if (!breadcrumbsHost || !bodyHost || !statusHost) {
8319 return void 0;
8320 }
8321 const state = {
8322 route: { kind: "root" },
8323 body: bodyHost,
8324 root,
8325 breadcrumbs: breadcrumbsHost,
8326 statusBar: statusHost,
8327 teardown: [],
8328 history: []
8329 };
8330 activeState = state;
8331 liveStates.set(bodyHost, state);
8332 const windowTeardowns = [];
8333 const dragManager = getDragManager();
8334 if (dragManager) {
8335 rejectIdCounter += 1;
8336 const deregister = dragManager.registerDropTarget({
8337 id: `${WINDOW_ID}-reject-${rejectIdCounter}`,
8338 element: body,
8339 accept: () => false,
8340 onDrop: () => {
8341 }
8342 });
8343 windowTeardowns.push(deregister);
8344 }
8345 windowTeardowns.push(() => closeAnyTileMenu());
8346 const initialRoute = pendingRoute ?? { kind: "root" };
8347 pendingRoute = null;
8348 navigate(state, initialRoute);
8349 return () => {
8350 clearTeardown(state);
8351 for (const fn of windowTeardowns) {
8352 try {
8353 fn();
8354 } catch {
8355 }
8356 }
8357 windowTeardowns.length = 0;
8358 liveStates.delete(bodyHost);
8359 if (activeState === state) {
8360 const next = liveStates.size > 0 ? Array.from(liveStates.values()).pop() : null;
8361 activeState = next;
8362 }
8363 };
8364 }
8365 const callback = (body) => {
8366 try {
8367 return renderInto(body);
8368 } catch (err) {
8369 console.error("[my-wordpress] render failed:", err);
8370 return void 0;
8371 }
8372 };
8373 window.desktopModeNativeWindows = window.desktopModeNativeWindows || {};
8374 window.desktopModeNativeWindows[WINDOW_ID] = callback;
8375 registerEntityKind("post", (host, entity) => {
8376 renderEntityList(asRenderState(host), entity);
8377 });
8378 registerEntityKind("user", (host, entity) => {
8379 renderUserEntityList(asRenderState(host), entity);
8380 });
8381 registerEntityKind("media", renderMediaList);
8382 function asRenderState(host) {
8383 const found = liveStates.get(host.body);
8384 if (found) {
8385 return found;
8386 }
8387 if (activeState && host.body === activeState.body) {
8388 return activeState;
8389 }
8390 throw new Error(
8391 "[my-wordpress] asRenderState: host body does not match any live render state."
8392 );
8393 }
8394 function openDetail(args) {
8395 const route = {
8396 kind: "detail",
8397 entityId: args.entityId,
8398 postId: args.postId,
8399 postTitle: args.postTitle
8400 };
8401 if (activeState) {
8402 navigate(activeState, route);
8403 return;
8404 }
8405 pendingRoute = route;
8406 const desktop = window.wp?.desktop;
8407 desktop?.openWindow?.(WINDOW_ID, { source: "my-wordpress/open-detail" });
8408 }
8409 function openMedia(args) {
8410 const route = {
8411 kind: "media-detail",
8412 entityId: "media",
8413 mediaId: args.mediaId,
8414 mediaTitle: args.mediaTitle ?? `#${args.mediaId}`
8415 };
8416 if (activeState) {
8417 navigate(activeState, route);
8418 return;
8419 }
8420 pendingRoute = route;
8421 const desktop = window.wp?.desktop;
8422 desktop?.openWindow?.(WINDOW_ID, { source: "my-wordpress/open-media" });
8423 }
8424 const desktopGlobal = window.wp?.desktop;
8425 if (desktopGlobal) {
8426 const pending = desktopGlobal.myWordpress?.__pendingKinds;
8427 if (Array.isArray(pending)) {
8428 for (const entry of pending) {
8429 try {
8430 entry.slot.unregister = registerEntityKind(
8431 entry.kind,
8432 entry.renderer
8433 );
8434 } catch (err) {
8435 console.error(
8436 `[my-wordpress] queued registerEntityKind('${entry.kind}') failed:`,
8437 err
8438 );
8439 }
8440 }
8441 pending.length = 0;
8442 }
8443 desktopGlobal.myWordpress = {
8444 openDetail,
8445 openMedia,
8446 registerEntityKind,
8447 trashEntity: trashEntityById
8448 };
8449 document.addEventListener(
8450 "desktop-mode-my-wordpress-entity-trashed",
8451 (e) => {
8452 const detail = e.detail;
8453 if (!detail || typeof detail.id !== "number") {
8454 return;
8455 }
8456 for (const state of liveStates.values()) {
8457 const tile = state.body.querySelector(
8458 `[data-entry-id="${detail.id}"]`
8459 );
8460 tile?.remove();
8461 }
8462 }
8463 );
8464 }
8465 })();
8466