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

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

8,425 lines 280.3 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 bridgePayload: payload.bridgePayload
1555 },
1556 ghost: {
1557 offsetX: e.clientX - rect.left,
1558 offsetY: e.clientY - rect.top
1559 }
1560 },
1561 origin: e,
1562 onClickOnly: onClick
1563 });
1564 });
1565 }
1566 function getDragManager() {
1567 const api = window.wp?.desktop?.dragManager;
1568 return api ?? null;
1569 }
1570 function stripTags(html2) {
1571 const div = document.createElement("div");
1572 div.innerHTML = html2;
1573 return (div.textContent ?? "").trim();
1574 }
1575 const renderers = /* @__PURE__ */ new Map();
1576 function registerEntityKind(kind, renderer) {
1577 if (typeof kind !== "string" || kind === "") {
1578 throw new TypeError(
1579 "[my-wordpress] registerEntityKind: kind must be a non-empty string."
1580 );
1581 }
1582 if (typeof renderer !== "function") {
1583 throw new TypeError(
1584 "[my-wordpress] registerEntityKind: renderer must be a function."
1585 );
1586 }
1587 renderers.set(kind, renderer);
1588 return () => {
1589 if (renderers.get(kind) === renderer) {
1590 renderers.delete(kind);
1591 }
1592 };
1593 }
1594 function getEntityRenderer(kind) {
1595 if (!kind) {
1596 return renderers.get("post");
1597 }
1598 return renderers.get(kind);
1599 }
1600 const DEBOUNCE_MS = 300;
1601 function renderListToolbar(options) {
1602 const host = document.createElement("div");
1603 host.className = "desktop-mode-my-wordpress__list-toolbar";
1604 const search = document.createElement("div");
1605 search.className = "desktop-mode-my-wordpress__list-toolbar-search";
1606 const input = document.createElement("input");
1607 input.type = "search";
1608 input.className = "desktop-mode-my-wordpress__list-toolbar-search-input";
1609 input.placeholder = options.placeholder ?? __("Search…", "desktop-mode");
1610 input.setAttribute(
1611 "aria-label",
1612 options.ariaLabel ?? options.placeholder ?? __("Search", "desktop-mode")
1613 );
1614 input.autocomplete = "off";
1615 input.spellcheck = false;
1616 if (options.initialValue) {
1617 input.value = options.initialValue;
1618 }
1619 search.appendChild(input);
1620 host.appendChild(search);
1621 let debounceId = null;
1622 let lastEmitted = options.initialValue ?? "";
1623 const emit = (raw) => {
1624 const normalized = raw.trim();
1625 if (normalized === lastEmitted) {
1626 return;
1627 }
1628 lastEmitted = normalized;
1629 options.onSearchChange(normalized);
1630 };
1631 const onInput = () => {
1632 if (debounceId !== null) {
1633 clearTimeout(debounceId);
1634 }
1635 debounceId = setTimeout(() => {
1636 debounceId = null;
1637 emit(input.value);
1638 }, DEBOUNCE_MS);
1639 };
1640 input.addEventListener("input", onInput);
1641 const onSearchEvent = () => {
1642 if (input.value === "") {
1643 if (debounceId !== null) {
1644 clearTimeout(debounceId);
1645 debounceId = null;
1646 }
1647 emit("");
1648 }
1649 };
1650 input.addEventListener("search", onSearchEvent);
1651 const onKeydown = (ev) => {
1652 if (ev.key === "Enter") {
1653 ev.preventDefault();
1654 if (debounceId !== null) {
1655 clearTimeout(debounceId);
1656 debounceId = null;
1657 }
1658 emit(input.value);
1659 }
1660 };
1661 input.addEventListener("keydown", onKeydown);
1662 return {
1663 host,
1664 getQuery: () => lastEmitted,
1665 destroy: () => {
1666 if (debounceId !== null) {
1667 clearTimeout(debounceId);
1668 debounceId = null;
1669 }
1670 input.removeEventListener("input", onInput);
1671 input.removeEventListener("search", onSearchEvent);
1672 input.removeEventListener("keydown", onKeydown);
1673 }
1674 };
1675 }
1676 const FILE_DROP_HOOKS = {
1677 /**
1678 * Action — fires after a successful upload. Payload:
1679 * `{ file: File, result: DropUploadResult, fields:
1680 * DropDialogFields, context: DropContext }`.
1681 *
1682 * The `file` field carries the same `File` reference that
1683 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
1684 * payload returned by the `BEFORE_UPLOAD` filter, in case a
1685 * plugin swapped the file). Subscribers tracking per-file
1686 * state — progress HUDs, sequence counters — should match on
1687 * this identity rather than the filename: two drops of
1688 * `photo.jpg` from different folders would otherwise route
1689 * each other's success event to the wrong row.
1690 *
1691 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
1692 * that destructured `{ result, fields, context }` keeps working.
1693 */
1694 AFTER_UPLOAD: "desktop-mode.drop.after-upload"
1695 };
1696 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 )}`;
1697 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
1698 constructor() {
1699 super(...arguments);
1700 this._onKey = (e) => {
1701 if (e.key === "Escape") {
1702 e.preventDefault();
1703 this._cancel();
1704 }
1705 if (e.key === "Enter" && !e.isComposing) {
1706 e.preventDefault();
1707 this._confirm();
1708 }
1709 };
1710 this._onBackdrop = (e) => {
1711 const path = e.composedPath();
1712 const original = path.length > 0 ? path[0] : e.target;
1713 if (original === this) {
1714 this._cancel();
1715 }
1716 };
1717 this._confirm = () => {
1718 this.emit("wpd-confirm", { confirmed: true });
1719 this.removeAttribute("open");
1720 };
1721 this._cancel = () => {
1722 this.emit("wpd-cancel", { confirmed: false });
1723 this.removeAttribute("open");
1724 };
1725 }
1726 connectedCallback() {
1727 super.connectedCallback();
1728 this.setAttribute("role", "dialog");
1729 this.setAttribute("aria-modal", "true");
1730 this.addEventListener("keydown", this._onKey);
1731 this.addEventListener("click", this._onBackdrop);
1732 }
1733 disconnectedCallback() {
1734 this.removeEventListener("keydown", this._onKey);
1735 this.removeEventListener("click", this._onBackdrop);
1736 }
1737 render() {
1738 const title = this.title ?? "";
1739 const message = this.message ?? "";
1740 const confirmLabel = this["confirm-label"] || "Confirm";
1741 const cancelLabel = this["cancel-label"] || "Cancel";
1742 const isDanger = this.hasAttribute("danger");
1743 const hideCancel = this.hasAttribute("hide-cancel");
1744 const isDismissable = this.hasAttribute("dismissable");
1745 return html`
1746 <div class="dialog" tabindex="-1">
1747 ${isDismissable ? html`<button
1748 type="button"
1749 class="close"
1750 aria-label="Close"
1751 @click=${() => this._cancel()}
1752 >&times;</button>` : html``}
1753 ${title ? html`<h2 class="title">${title}</h2>` : html``}
1754 ${message ? html`<p class="message">${message}</p>` : html``}
1755 <div class="actions">
1756 ${hideCancel ? html`` : html`<button
1757 type="button"
1758 class="btn btn--secondary"
1759 @click=${() => this._cancel()}
1760 >
1761 ${cancelLabel}
1762 </button>`}
1763 <button
1764 type="button"
1765 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
1766 @click=${() => this._confirm()}
1767 >
1768 ${confirmLabel}
1769 </button>
1770 </div>
1771 </div>
1772 `;
1773 }
1774 };
1775 _WpdConfirmDialog.props = [
1776 "open",
1777 "title",
1778 "message",
1779 "confirm-label",
1780 "cancel-label",
1781 "danger",
1782 "hide-cancel",
1783 "dismissable"
1784 ];
1785 _WpdConfirmDialog.styles = [dialogStyles];
1786 _WpdConfirmDialog.help = {
1787 title: "Confirm dialog",
1788 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.",
1789 status: "experimental",
1790 since: "0.9.0",
1791 props: [
1792 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
1793 { name: "title", type: "string", description: "Heading shown at the top." },
1794 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
1795 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
1796 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
1797 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
1798 { 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." },
1799 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
1800 ],
1801 events: [
1802 {
1803 name: "wpd-confirm",
1804 description: "Fires on confirm. Detail: `{ confirmed: true }`."
1805 },
1806 {
1807 name: "wpd-cancel",
1808 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
1809 }
1810 ]
1811 };
1812 let WpdConfirmDialog = _WpdConfirmDialog;
1813 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
1814 function wpdConfirm(options) {
1815 return new Promise((resolve) => {
1816 const dialog = document.createElement("wpd-confirm-dialog");
1817 dialog.setAttribute("open", "");
1818 if (options.title) {
1819 dialog.setAttribute("title", options.title);
1820 }
1821 dialog.setAttribute("message", options.message);
1822 if (options.confirmLabel) {
1823 dialog.setAttribute("confirm-label", options.confirmLabel);
1824 }
1825 if (options.cancelLabel) {
1826 dialog.setAttribute("cancel-label", options.cancelLabel);
1827 }
1828 {
1829 dialog.setAttribute("danger", "");
1830 }
1831 if (options.hideCancel) {
1832 dialog.setAttribute("hide-cancel", "");
1833 }
1834 if (options.dismissable) {
1835 dialog.setAttribute("dismissable", "");
1836 }
1837 const cleanup = (ok) => {
1838 dialog.remove();
1839 resolve(ok);
1840 };
1841 dialog.addEventListener("wpd-confirm", () => cleanup(true));
1842 dialog.addEventListener("wpd-cancel", () => cleanup(false));
1843 document.body.appendChild(dialog);
1844 const inner = dialog.shadowRoot?.querySelector(".dialog");
1845 (inner ?? dialog).focus?.();
1846 });
1847 }
1848 const HOOK_PREFIX = "desktop-mode.activity.";
1849 function hookName(channel) {
1850 return `${HOOK_PREFIX}${String(channel)}`;
1851 }
1852 let subscribeSeq = 0;
1853 const activity = {
1854 publish(channel, payload) {
1855 doAction(hookName(channel), payload);
1856 },
1857 subscribe(channel, cb) {
1858 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
1859 const hook = hookName(channel);
1860 addAction(
1861 hook,
1862 ns,
1863 (payload) => cb(payload)
1864 );
1865 let removed = false;
1866 return () => {
1867 if (removed) {
1868 return;
1869 }
1870 removed = true;
1871 removeAction(hook, ns);
1872 };
1873 },
1874 filter(channel, value, ...args) {
1875 return applyFilters(hookName(channel), value, ...args);
1876 }
1877 };
1878 const DEFAULT_DURATION_MS = 4e3;
1879 const FADE_OUT_MS = 200;
1880 function showToast$1(options) {
1881 const intent = activity.filter(
1882 "desktop-mode/toast-requested",
1883 { ...options }
1884 );
1885 if (!intent || intent.cancel === true) {
1886 return () => void 0;
1887 }
1888 let dismissRequested = false;
1889 let realDismiss = null;
1890 openWithShellOverlays(
1891 () => !dismissRequested,
1892 () => {
1893 realDismiss = renderToast(intent);
1894 }
1895 );
1896 return () => {
1897 dismissRequested = true;
1898 if (realDismiss) {
1899 realDismiss();
1900 }
1901 };
1902 }
1903 function renderToast(intent) {
1904 const container = ensureContainer();
1905 const toast = document.createElement("wpd-toast");
1906 toast.textContent = intent.message;
1907 if (intent.action) {
1908 toast.setAttribute("action", intent.action.label);
1909 toast.addEventListener("wpd-toast-action", () => {
1910 intent.action?.onClick();
1911 dismiss();
1912 });
1913 }
1914 container.appendChild(toast);
1915 let dismissed = false;
1916 let dismissTimer = null;
1917 const dismiss = () => {
1918 if (dismissed) {
1919 return;
1920 }
1921 dismissed = true;
1922 if (dismissTimer !== null) {
1923 window.clearTimeout(dismissTimer);
1924 dismissTimer = null;
1925 }
1926 toast.setAttribute("state", "out");
1927 window.setTimeout(() => {
1928 toast.remove();
1929 }, FADE_OUT_MS);
1930 };
1931 requestAnimationFrame(() => {
1932 toast.setAttribute("state", "in");
1933 });
1934 dismissTimer = window.setTimeout(
1935 dismiss,
1936 intent.duration ?? DEFAULT_DURATION_MS
1937 );
1938 activity.publish("desktop-mode/toast-shown", { ...intent });
1939 return dismiss;
1940 }
1941 function ensureContainer() {
1942 const existing = document.querySelector(
1943 "wpd-toast-container"
1944 );
1945 if (existing) {
1946 return existing;
1947 }
1948 const el = document.createElement("wpd-toast-container");
1949 document.body.appendChild(el);
1950 return el;
1951 }
1952 const FALLBACK_BASE = "http://localhost/";
1953 function joinRestUrl(restRoot, path) {
1954 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
1955 const url = new URL(restRoot, base);
1956 const trimmed = path.replace(/^\/+/, "");
1957 const queryAt = trimmed.indexOf("?");
1958 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
1959 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
1960 if (url.searchParams.has("rest_route")) {
1961 const existing = url.searchParams.get("rest_route") ?? "/";
1962 const prefix = existing.endsWith("/") ? existing : existing + "/";
1963 url.searchParams.set("rest_route", prefix + route);
1964 } else {
1965 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
1966 url.pathname = pathname + route;
1967 }
1968 if (extraQuery) {
1969 const extras = new URLSearchParams(extraQuery);
1970 extras.forEach((value, key) => {
1971 url.searchParams.append(key, value);
1972 });
1973 }
1974 return url.toString();
1975 }
1976 const NONCE_HEADER = "X-WP-Nonce";
1977 function injectRestNonce(input, init) {
1978 const nonce = readRestNonce();
1979 if (!nonce) {
1980 return init;
1981 }
1982 const url = resolveUrl(input);
1983 if (!url || !isSameOriginRestUrl(url)) {
1984 return init;
1985 }
1986 const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
1987 const headers = new Headers(baseHeaders ?? {});
1988 if (headers.has(NONCE_HEADER)) {
1989 return init;
1990 }
1991 headers.set(NONCE_HEADER, nonce);
1992 return { ...init ?? {}, headers };
1993 }
1994 function readRestNonce() {
1995 if (typeof window === "undefined") {
1996 return void 0;
1997 }
1998 const cfg = window.desktopModeConfig;
1999 const value = cfg?.restNonce;
2000 return typeof value === "string" && value.length > 0 ? value : void 0;
2001 }
2002 function resolveUrl(input) {
2003 try {
2004 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
2005 if (typeof input === "string") {
2006 return new URL(input, base);
2007 }
2008 if (input instanceof URL) {
2009 return input;
2010 }
2011 if (typeof Request !== "undefined" && input instanceof Request) {
2012 return new URL(input.url, base);
2013 }
2014 return null;
2015 } catch {
2016 return null;
2017 }
2018 }
2019 function isSameOriginRestUrl(url) {
2020 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
2021 return false;
2022 }
2023 if (url.pathname.includes("/wp-json/")) {
2024 return true;
2025 }
2026 if (url.searchParams.has("rest_route")) {
2027 return true;
2028 }
2029 return false;
2030 }
2031 function trackedFetch(input, init, opts = {}) {
2032 const fn = window.wp?.desktop?.fetch;
2033 if (typeof fn === "function") {
2034 return fn(input, init, opts);
2035 }
2036 const finalInit = injectRestNonce(input, init);
2037 return fetch(input, finalInit);
2038 }
2039 const WINDOW_ID$2 = "desktop-mode-my-wordpress";
2040 function getConfig() {
2041 const store = window.desktopModeWindowConfig;
2042 const cfg = store ? store[WINDOW_ID$2] : void 0;
2043 if (!cfg) {
2044 throw new Error(
2045 "[desktop-mode-my-wordpress] config blob missing — was the window opened without registration?"
2046 );
2047 }
2048 return cfg;
2049 }
2050 function getEntity(id) {
2051 return getConfig().entities.find((e) => e.id === id);
2052 }
2053 function buildUrl$1(path) {
2054 return joinRestUrl(getConfig().restRoot, path);
2055 }
2056 async function shellFetch$1(input, init) {
2057 return trackedFetch(input, init, {
2058 windowId: WINDOW_ID$2,
2059 source: "desktop-mode/my-wordpress"
2060 });
2061 }
2062 async function fetchEntityList(entity, params) {
2063 const cfg = getConfig();
2064 const url = new URL(buildUrl$1(entity.restPath));
2065 url.searchParams.set("page", String(params.page));
2066 url.searchParams.set("per_page", String(params.perPage));
2067 url.searchParams.set(
2068 "_fields",
2069 "id,title,excerpt,date,status,featured_media,link,desktop_mode_lock,_links,_embedded"
2070 );
2071 url.searchParams.set("_embed", "wp:featuredmedia");
2072 url.searchParams.set("status", "publish,future,draft,pending,private");
2073 if (params.search) {
2074 url.searchParams.set("search", params.search);
2075 }
2076 const response = await shellFetch$1(url.toString(), {
2077 method: "GET",
2078 credentials: "same-origin",
2079 headers: {
2080 "X-WP-Nonce": cfg.restNonce,
2081 Accept: "application/json"
2082 },
2083 signal: params.signal
2084 });
2085 if (!response.ok) {
2086 throw new Error(
2087 await readErrorMessage$1(response, "Failed to load list")
2088 );
2089 }
2090 const items = await response.json();
2091 const total = Number(response.headers.get("X-WP-Total") ?? items.length);
2092 const totalPages = Number(
2093 response.headers.get("X-WP-TotalPages") ?? 1
2094 );
2095 return { items, total, totalPages };
2096 }
2097 async function fetchEntityDetail(entity, id) {
2098 const cfg = getConfig();
2099 const url = new URL(buildUrl$1(`${entity.restPath}/${id}`));
2100 url.searchParams.set(
2101 "_fields",
2102 "id,title,content,excerpt,date,modified,status,link,author,featured_media,categories,tags,comment_status,desktop_mode_contributors,desktop_mode_attached_media,_links,_embedded"
2103 );
2104 url.searchParams.set("_embed", "author,wp:term,wp:featuredmedia,replies");
2105 const response = await shellFetch$1(url.toString(), {
2106 method: "GET",
2107 credentials: "same-origin",
2108 headers: {
2109 "X-WP-Nonce": cfg.restNonce,
2110 Accept: "application/json"
2111 }
2112 });
2113 if (!response.ok) {
2114 throw new Error(
2115 await readErrorMessage$1(response, "Failed to load entry")
2116 );
2117 }
2118 return await response.json();
2119 }
2120 async function trashEntity(entity, id) {
2121 const cfg = getConfig();
2122 const url = buildUrl$1(`${entity.restPath}/${id}`);
2123 const response = await shellFetch$1(url, {
2124 method: "DELETE",
2125 credentials: "same-origin",
2126 headers: {
2127 "X-WP-Nonce": cfg.restNonce,
2128 Accept: "application/json"
2129 }
2130 });
2131 if (!response.ok) {
2132 throw new Error(
2133 await readErrorMessage$1(response, "Failed to move to trash")
2134 );
2135 }
2136 }
2137 async function readErrorMessage$1(response, fallback) {
2138 let message = `${response.status} ${response.statusText || fallback}`;
2139 try {
2140 const json = await response.json();
2141 if (json && typeof json.message === "string") {
2142 message = json.message;
2143 }
2144 } catch {
2145 }
2146 return message;
2147 }
2148 async function fetchEntityTotal(entity) {
2149 const cfg = getConfig();
2150 const buildRequestUrl = (withWho) => {
2151 const url = new URL(buildUrl$1(entity.restPath));
2152 url.searchParams.set("page", "1");
2153 url.searchParams.set("per_page", "1");
2154 url.searchParams.set("_fields", "id");
2155 if (entity.kind === "user") {
2156 if (withWho) {
2157 url.searchParams.set("who", "authors");
2158 }
2159 } else if (entity.kind === "media") {
2160 url.searchParams.set("status", "inherit");
2161 } else {
2162 url.searchParams.set("status", "publish,future,draft,pending,private");
2163 }
2164 return url.toString();
2165 };
2166 const send = (target) => shellFetch$1(target, {
2167 method: "GET",
2168 credentials: "same-origin",
2169 headers: {
2170 "X-WP-Nonce": cfg.restNonce,
2171 Accept: "application/json"
2172 }
2173 });
2174 let response = await send(buildRequestUrl(false));
2175 if (response.status === 403 && entity.kind === "user") {
2176 response = await send(buildRequestUrl(true));
2177 }
2178 if (!response.ok) {
2179 throw new Error(await readErrorMessage$1(response, "Failed to count"));
2180 }
2181 await response.json().catch(() => null);
2182 const raw = response.headers.get("X-WP-Total");
2183 const n = raw ? Number(raw) : NaN;
2184 return Number.isFinite(n) ? n : 0;
2185 }
2186 async function fetchUserList(entity, params) {
2187 const cfg = getConfig();
2188 const buildRequestUrl = (mode) => {
2189 const url = new URL(buildUrl$1(entity.restPath));
2190 url.searchParams.set("page", String(params.page));
2191 url.searchParams.set("per_page", String(params.perPage));
2192 url.searchParams.set(
2193 "_fields",
2194 "id,name,slug,description,link,avatar_urls,desktop_mode_summary"
2195 );
2196 url.searchParams.set("orderby", "name");
2197 url.searchParams.set("order", "asc");
2198 if (mode === "edit") {
2199 url.searchParams.set("context", "edit");
2200 } else {
2201 url.searchParams.set("who", "authors");
2202 }
2203 if (params.search) {
2204 url.searchParams.set("search", params.search);
2205 }
2206 return url.toString();
2207 };
2208 const send = (target) => shellFetch$1(target, {
2209 method: "GET",
2210 credentials: "same-origin",
2211 headers: {
2212 "X-WP-Nonce": cfg.restNonce,
2213 Accept: "application/json"
2214 },
2215 signal: params.signal
2216 });
2217 let response = await send(buildRequestUrl("edit"));
2218 if (response.status === 403) {
2219 response = await send(buildRequestUrl("authors"));
2220 }
2221 if (!response.ok) {
2222 throw new Error(await readErrorMessage$1(response, "Failed to load users"));
2223 }
2224 const items = await response.json();
2225 const total = Number(response.headers.get("X-WP-Total") ?? items.length);
2226 const totalPages = Number(
2227 response.headers.get("X-WP-TotalPages") ?? 1
2228 );
2229 return { items, total, totalPages };
2230 }
2231 function fetchUserFootprint(userId) {
2232 return getJson(
2233 buildUrl$1(`desktop-mode/v1/user-footprint/${userId}`)
2234 );
2235 }
2236 function buildEditUserUrl(id) {
2237 const cfg = getConfig();
2238 const base = cfg.editUserUrlBase || cfg.editPostUrlBase;
2239 const sep = base.includes("?") ? "&" : "?";
2240 return `${base}${sep}user_id=${encodeURIComponent(String(id))}`;
2241 }
2242 function buildEditUrl(id) {
2243 const cfg = getConfig();
2244 const base = cfg.editPostUrlBase;
2245 const sep = base.includes("?") ? "&" : "?";
2246 return `${base}${sep}post=${encodeURIComponent(String(id))}&action=edit`;
2247 }
2248 async function getJson(url) {
2249 const cfg = getConfig();
2250 const response = await shellFetch$1(url, {
2251 method: "GET",
2252 credentials: "same-origin",
2253 headers: {
2254 "X-WP-Nonce": cfg.restNonce,
2255 Accept: "application/json"
2256 }
2257 });
2258 if (!response.ok) {
2259 throw new Error(await readErrorMessage$1(response, "Failed to load"));
2260 }
2261 return await response.json();
2262 }
2263 function fetchUserStats(id) {
2264 return getJson(buildUrl$1(`desktop-mode/v1/user-stats/${id}`));
2265 }
2266 function fetchTermStats(taxonomy, id) {
2267 const slug = taxonomy.replace(/[^a-zA-Z0-9_-]/g, "");
2268 return getJson(
2269 buildUrl$1(`desktop-mode/v1/term-stats/${slug}/${id}`)
2270 );
2271 }
2272 function fetchCommentStats(id) {
2273 return getJson(
2274 buildUrl$1(`desktop-mode/v1/comment-stats/${id}`)
2275 );
2276 }
2277 function fetchUser(id) {
2278 return getJson(
2279 buildUrl$1(`wp/v2/users/${id}?context=edit&_fields=id,name,slug,description,avatar_urls,link`)
2280 );
2281 }
2282 function fetchComments(postId) {
2283 return getJson(
2284 buildUrl$1(
2285 `wp/v2/comments?post=${postId}&per_page=100&_fields=id,post,author,author_name,author_avatar_urls,date,content,status,parent`
2286 )
2287 );
2288 }
2289 function fetchTerms(taxonomy, ids) {
2290 if (ids.length === 0) {
2291 return Promise.resolve([]);
2292 }
2293 return getJson(
2294 buildUrl$1(
2295 `wp/v2/${taxonomy}?include=${ids.join(",")}&per_page=100&_fields=id,name,slug,taxonomy,description,count,link`
2296 )
2297 );
2298 }
2299 function fetchAttachedMedia(postId) {
2300 return getJson(
2301 buildUrl$1(
2302 `wp/v2/media?parent=${postId}&per_page=100&_fields=id,title,source_url,mime_type,alt_text,date,media_details`
2303 )
2304 );
2305 }
2306 function fetchMediaByIds(ids) {
2307 const unique = Array.from(new Set(ids.filter((id) => id > 0)));
2308 if (unique.length === 0) {
2309 return Promise.resolve([]);
2310 }
2311 return getJson(
2312 buildUrl$1(
2313 `wp/v2/media?include=${unique.join(",")}&per_page=${unique.length}&_fields=id,title,source_url,mime_type,alt_text,date,media_details`
2314 )
2315 );
2316 }
2317 function fetchRevisions(entity, postId) {
2318 return getJson(
2319 buildUrl$1(
2320 `${entity.restPath}/${postId}/revisions?_fields=id,date,modified,author,title`
2321 )
2322 );
2323 }
2324 function fetchRevision(entity, postId, revisionId) {
2325 return getJson(
2326 buildUrl$1(
2327 `${entity.restPath}/${postId}/revisions/${revisionId}?_fields=id,date,modified,author,title,content,excerpt`
2328 )
2329 );
2330 }
2331 const WINDOW_ID$1 = "desktop-mode-my-wordpress";
2332 function shellFetch(input, init) {
2333 return trackedFetch(input, init, {
2334 windowId: WINDOW_ID$1,
2335 source: "desktop-mode/my-wordpress"
2336 });
2337 }
2338 function buildUrl(path) {
2339 return joinRestUrl(getConfig().restRoot, path);
2340 }
2341 async function readErrorMessage(response, fallback) {
2342 let message = `${response.status} ${response.statusText || fallback}`;
2343 try {
2344 const json = await response.json();
2345 if (json && typeof json.message === "string") {
2346 message = json.message;
2347 }
2348 } catch {
2349 }
2350 return message;
2351 }
2352 async function fetchMediaPage(entity, params) {
2353 const cfg = getConfig();
2354 const url = new URL(buildUrl(entity.restPath));
2355 url.searchParams.set("page", String(params.page));
2356 url.searchParams.set("per_page", String(params.perPage));
2357 url.searchParams.set(
2358 "_fields",
2359 "id,title,date,mime_type,source_url,alt_text,caption,description,author,media_details,_embedded"
2360 );
2361 url.searchParams.set("_embed", "author");
2362 url.searchParams.set("orderby", "date");
2363 url.searchParams.set("order", "desc");
2364 url.searchParams.set("status", "inherit");
2365 if (params.search) {
2366 url.searchParams.set("search", params.search);
2367 }
2368 const response = await shellFetch(url.toString(), {
2369 method: "GET",
2370 credentials: "same-origin",
2371 headers: {
2372 "X-WP-Nonce": cfg.restNonce,
2373 Accept: "application/json"
2374 },
2375 signal: params.signal
2376 });
2377 if (!response.ok) {
2378 throw new Error(
2379 await readErrorMessage(response, "Failed to load media")
2380 );
2381 }
2382 const items = await response.json();
2383 const total = Number(response.headers.get("X-WP-Total") ?? items.length);
2384 const totalPages = Number(
2385 response.headers.get("X-WP-TotalPages") ?? 1
2386 );
2387 return { items, total, totalPages };
2388 }
2389 async function fetchMediaItem(mediaId) {
2390 const cfg = getConfig();
2391 const url = new URL(buildUrl(`wp/v2/media/${mediaId}`));
2392 url.searchParams.set(
2393 "_fields",
2394 "id,title,date,mime_type,source_url,alt_text,caption,description,author,media_details,_embedded"
2395 );
2396 url.searchParams.set("_embed", "author");
2397 const response = await shellFetch(url.toString(), {
2398 method: "GET",
2399 credentials: "same-origin",
2400 headers: {
2401 "X-WP-Nonce": cfg.restNonce,
2402 Accept: "application/json"
2403 }
2404 });
2405 if (!response.ok) {
2406 throw new Error(
2407 await readErrorMessage(response, "Failed to load media item")
2408 );
2409 }
2410 return await response.json();
2411 }
2412 async function deleteMediaItem(mediaId) {
2413 const cfg = getConfig();
2414 const url = new URL(buildUrl(`wp/v2/media/${mediaId}`));
2415 url.searchParams.set("force", "true");
2416 const response = await shellFetch(url.toString(), {
2417 method: "DELETE",
2418 credentials: "same-origin",
2419 headers: {
2420 "X-WP-Nonce": cfg.restNonce,
2421 Accept: "application/json"
2422 }
2423 });
2424 if (!response.ok) {
2425 throw new Error(
2426 await readErrorMessage(response, "Failed to delete media item")
2427 );
2428 }
2429 }
2430 async function fetchMediaUsage(mediaId) {
2431 const cfg = getConfig();
2432 const response = await shellFetch(
2433 buildUrl(`desktop-mode/v1/media-usage/${mediaId}`),
2434 {
2435 method: "GET",
2436 credentials: "same-origin",
2437 headers: {
2438 "X-WP-Nonce": cfg.restNonce,
2439 Accept: "application/json"
2440 }
2441 }
2442 );
2443 if (!response.ok) {
2444 throw new Error(
2445 await readErrorMessage(response, "Failed to load media usage")
2446 );
2447 }
2448 return await response.json();
2449 }
2450 const MIME_DASHICON_FALLBACK = "dashicons-media-default";
2451 const MIME_DASHICON_MAP = [
2452 { test: /^image\//, icon: "dashicons-format-image" },
2453 { test: /^video\//, icon: "dashicons-format-video" },
2454 { test: /^audio\//, icon: "dashicons-format-audio" },
2455 { test: /pdf$/, icon: "dashicons-media-document" },
2456 { test: /^application\/(zip|x-tar|x-rar|x-7z)/, icon: "dashicons-media-archive" },
2457 { test: /spreadsheet|excel/, icon: "dashicons-media-spreadsheet" },
2458 { test: /word|document/, icon: "dashicons-media-document" },
2459 { test: /^text\//, icon: "dashicons-media-text" }
2460 ];
2461 function dashiconForMime(mime) {
2462 for (const entry of MIME_DASHICON_MAP) {
2463 if (entry.test.test(mime)) {
2464 return entry.icon;
2465 }
2466 }
2467 return MIME_DASHICON_FALLBACK;
2468 }
2469 function mimeGroup(mime) {
2470 if (mime.startsWith("image/")) {
2471 return "image";
2472 }
2473 if (mime.startsWith("video/")) {
2474 return "video";
2475 }
2476 if (mime.startsWith("audio/")) {
2477 return "audio";
2478 }
2479 return "doc";
2480 }
2481 function formatBytes(bytes) {
2482 if (!bytes || !Number.isFinite(bytes)) {
2483 return "";
2484 }
2485 if (bytes < 1024) {
2486 return `${bytes} B`;
2487 }
2488 const units = ["KB", "MB", "GB", "TB"];
2489 let value = bytes / 1024;
2490 let unit = 0;
2491 while (value >= 1024 && unit < units.length - 1) {
2492 value /= 1024;
2493 unit += 1;
2494 }
2495 return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`;
2496 }
2497 function formatDate$1(iso) {
2498 if (!iso) {
2499 return "";
2500 }
2501 const d = new Date(iso);
2502 if (Number.isNaN(d.valueOf())) {
2503 return iso;
2504 }
2505 return d.toLocaleDateString(void 0, {
2506 year: "numeric",
2507 month: "short",
2508 day: "numeric"
2509 });
2510 }
2511 function buildMediaVisual(media) {
2512 const wrap = document.createElement("div");
2513 wrap.className = "desktop-mode-my-wordpress__media-visual";
2514 const group = mimeGroup(media.mime_type);
2515 if (group === "image") {
2516 const img = document.createElement("img");
2517 const sizes = media.media_details?.sizes;
2518 img.src = sizes?.large?.source_url ?? sizes?.medium_large?.source_url ?? sizes?.medium?.source_url ?? media.source_url;
2519 img.alt = media.alt_text ?? stripTags(media.title.rendered);
2520 img.loading = "lazy";
2521 img.decoding = "async";
2522 img.className = "desktop-mode-my-wordpress__media-image";
2523 wrap.appendChild(img);
2524 return wrap;
2525 }
2526 if (group === "video") {
2527 const video = document.createElement("video");
2528 video.controls = true;
2529 video.preload = "metadata";
2530 video.src = media.source_url;
2531 const sizes = media.media_details?.sizes;
2532 const poster = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? "";
2533 if (poster) {
2534 video.poster = poster;
2535 }
2536 video.className = "desktop-mode-my-wordpress__media-video";
2537 wrap.appendChild(video);
2538 return wrap;
2539 }
2540 if (group === "audio") {
2541 const stack = document.createElement("div");
2542 stack.className = "desktop-mode-my-wordpress__media-audio-stack";
2543 const icon2 = document.createElement("span");
2544 icon2.className = "desktop-mode-my-wordpress__media-fallback-icon dashicons " + dashiconForMime(media.mime_type);
2545 icon2.setAttribute("aria-hidden", "true");
2546 stack.appendChild(icon2);
2547 const audio = document.createElement("audio");
2548 audio.controls = true;
2549 audio.preload = "metadata";
2550 audio.src = media.source_url;
2551 audio.className = "desktop-mode-my-wordpress__media-audio";
2552 stack.appendChild(audio);
2553 wrap.appendChild(stack);
2554 return wrap;
2555 }
2556 const icon = document.createElement("span");
2557 icon.className = "desktop-mode-my-wordpress__media-fallback-icon dashicons " + dashiconForMime(media.mime_type);
2558 icon.setAttribute("aria-hidden", "true");
2559 wrap.appendChild(icon);
2560 const link = document.createElement("a");
2561 link.href = media.source_url;
2562 link.target = "_blank";
2563 link.rel = "noopener noreferrer";
2564 link.className = "desktop-mode-my-wordpress__media-doc-link";
2565 link.textContent = __("Open file", "desktop-mode");
2566 wrap.appendChild(link);
2567 return wrap;
2568 }
2569 function buildMetaRow(label, value) {
2570 if (typeof value === "string" && value.trim() === "") {
2571 return null;
2572 }
2573 const dt = document.createElement("dt");
2574 dt.className = "desktop-mode-my-wordpress__media-meta-term";
2575 dt.textContent = label;
2576 const dd = document.createElement("dd");
2577 dd.className = "desktop-mode-my-wordpress__media-meta-value";
2578 if (typeof value === "string") {
2579 dd.textContent = value;
2580 } else {
2581 dd.appendChild(value);
2582 }
2583 return [dt, dd];
2584 }
2585 function buildMetadataGrid(media) {
2586 const grid = document.createElement("dl");
2587 grid.className = "desktop-mode-my-wordpress__media-meta";
2588 const filename = media.media_details?.file ? media.media_details.file.split("/").pop() ?? "" : media.source_url.split("/").pop() ?? "";
2589 const dims = media.media_details?.width && media.media_details?.height ? `${media.media_details.width} × ${media.media_details.height}` : "";
2590 const filesize = formatBytes(media.media_details?.filesize);
2591 const uploaded = formatDate$1(media.date);
2592 const uploader = media._embedded?.author?.[0]?.name ?? "";
2593 const alt = (media.alt_text ?? "").trim();
2594 const caption = stripTags(media.caption?.rendered ?? "");
2595 const description = stripTags(media.description?.rendered ?? "");
2596 const rows = [
2597 buildMetaRow(__("Filename", "desktop-mode"), filename),
2598 buildMetaRow(__("Type", "desktop-mode"), media.mime_type),
2599 buildMetaRow(__("Dimensions", "desktop-mode"), dims),
2600 buildMetaRow(__("File size", "desktop-mode"), filesize),
2601 buildMetaRow(__("Uploaded", "desktop-mode"), uploaded),
2602 buildMetaRow(__("Uploader", "desktop-mode"), uploader),
2603 buildMetaRow(__("Alt text", "desktop-mode"), alt),
2604 buildMetaRow(__("Caption", "desktop-mode"), caption),
2605 buildMetaRow(__("Description", "desktop-mode"), description)
2606 ];
2607 for (const pair of rows) {
2608 if (pair) {
2609 grid.append(...pair);
2610 }
2611 }
2612 return grid;
2613 }
2614 function fireSlot(host, slot, entityId, kind, item) {
2615 doAction(
2616 "desktop-mode.my-wordpress.preview-extras",
2617 {
2618 slot,
2619 container: host,
2620 entityId,
2621 kind,
2622 item
2623 }
2624 );
2625 }
2626 function resolvePreviewActions(descriptors, ctx) {
2627 const scoped = descriptors.filter((a) => {
2628 if (a.sections && a.sections.length > 0) {
2629 if (!a.sections.includes(ctx.entityId) && !a.sections.includes("*")) {
2630 return false;
2631 }
2632 }
2633 if (a.mime) {
2634 if (!ctx.mime) {
2635 return false;
2636 }
2637 try {
2638 const re = new RegExp(a.mime);
2639 if (!re.test(ctx.mime)) {
2640 return false;
2641 }
2642 } catch {
2643 return false;
2644 }
2645 }
2646 return true;
2647 });
2648 const merged = applyFilters("desktop-mode.my-wordpress.preview-actions", scoped, ctx);
2649 return Array.isArray(merged) ? merged : scoped;
2650 }
2651 function buildActionRow(actions, ctx) {
2652 const visible = actions.filter(
2653 (a) => typeof a.isVisible === "function" ? a.isVisible(ctx) : true
2654 );
2655 if (visible.length === 0) {
2656 return null;
2657 }
2658 const row = document.createElement("div");
2659 row.className = "desktop-mode-my-wordpress__media-actions";
2660 row.setAttribute("role", "toolbar");
2661 for (const action of visible) {
2662 const btn = document.createElement("wpd-button");
2663 btn.setAttribute("variant", "secondary");
2664 btn.dataset.actionId = action.id;
2665 if (action.icon) {
2666 btn.setAttribute("icon", action.icon);
2667 }
2668 btn.textContent = action.label;
2669 btn.addEventListener("click", () => {
2670 if (typeof action.onSelect === "function") {
2671 try {
2672 void action.onSelect(ctx);
2673 } catch {
2674 console.error(
2675 `[my-wordpress] preview action ${action.id} threw.`
2676 );
2677 }
2678 }
2679 });
2680 row.appendChild(btn);
2681 }
2682 return row;
2683 }
2684 function renderMediaPreview(host, media, opts) {
2685 host.replaceChildren();
2686 const pane = document.createElement("div");
2687 pane.className = "desktop-mode-my-wordpress__media-pane";
2688 const header = document.createElement("header");
2689 header.className = "desktop-mode-my-wordpress__media-header";
2690 const heading = document.createElement("h2");
2691 heading.className = "desktop-mode-my-wordpress__media-title";
2692 heading.textContent = stripTags(media.title.rendered) || __("(no title)", "desktop-mode");
2693 header.appendChild(heading);
2694 pane.appendChild(header);
2695 const item = media;
2696 const ctx = {
2697 entityId: opts.entityId,
2698 kind: "media",
2699 mime: media.mime_type,
2700 item
2701 };
2702 fireSlot(header, "header", opts.entityId, "media", item);
2703 pane.appendChild(buildMediaVisual(media));
2704 const meta = buildMetadataGrid(media);
2705 pane.appendChild(meta);
2706 fireSlot(meta, "meta", opts.entityId, "media", item);
2707 const resolved = resolvePreviewActions(opts.previewActions, ctx);
2708 const actionRow = buildActionRow(resolved, ctx);
2709 if (actionRow) {
2710 pane.appendChild(actionRow);
2711 }
2712 if (opts.onOpenDetail) {
2713 const footer = document.createElement("footer");
2714 footer.className = "desktop-mode-my-wordpress__article-footer";
2715 const drillBtn = document.createElement("wpd-button");
2716 drillBtn.setAttribute("variant", "primary");
2717 drillBtn.textContent = __("See where this is used", "desktop-mode");
2718 drillBtn.title = __(
2719 "Show the posts, pages, and custom-post-type entries that reference this file.",
2720 "desktop-mode"
2721 );
2722 drillBtn.addEventListener("click", () => opts.onOpenDetail?.());
2723 footer.appendChild(drillBtn);
2724 pane.appendChild(footer);
2725 fireSlot(footer, "footer", opts.entityId, "media", item);
2726 } else {
2727 const footer = document.createElement("div");
2728 footer.className = "desktop-mode-my-wordpress__media-footer";
2729 pane.appendChild(footer);
2730 fireSlot(footer, "footer", opts.entityId, "media", item);
2731 }
2732 host.appendChild(pane);
2733 }
2734 const lastQueryByMediaEntity = /* @__PURE__ */ new Map();
2735 function describeCount(ctx) {
2736 if (ctx.total === 0 && ctx.loaded === 0) {
2737 return __("No media yet.", "desktop-mode");
2738 }
2739 if (ctx.total > ctx.loaded && ctx.loaded > 0) {
2740 return sprintf(
2741 // translators: 1: visible item count, 2: total item count.
2742 __("%1$d of %2$d items", "desktop-mode"),
2743 ctx.loaded,
2744 ctx.total
2745 );
2746 }
2747 const n = Math.max(ctx.total, ctx.loaded);
2748 return sprintf(
2749 // translators: %d is a count of media items.
2750 _n("%d item", "%d items", n),
2751 n
2752 );
2753 }
2754 function paintStatus$2(ctx) {
2755 const segments = [
2756 {
2757 id: "count",
2758 label: describeCount(ctx),
2759 align: "start",
2760 sort: 10
2761 }
2762 ];
2763 if (ctx.totalPages > 1) {
2764 segments.push({
2765 id: "page",
2766 label: sprintf(
2767 // translators: 1: current page, 2: total pages.
2768 __("Page %1$d of %2$d", "desktop-mode"),
2769 Math.max(ctx.page, 1),
2770 ctx.totalPages
2771 ),
2772 align: "end",
2773 sort: 10
2774 });
2775 }
2776 const filtered = applyFilters(
2777 "desktop-mode.my-wordpress.status-bar",
2778 segments,
2779 { view: "list", entityId: ctx.entity.id }
2780 );
2781 renderStatusBarSegments(
2782 ctx.statusBar,
2783 Array.isArray(filtered) ? filtered : segments
2784 );
2785 }
2786 function buildMediaTile(ctx, media) {
2787 const titleText = stripTags(media.title.rendered) || __("(no title)", "desktop-mode");
2788 const sizes = media.media_details?.sizes;
2789 const thumbUrl = media.mime_type.startsWith("image/") ? sizes?.thumbnail?.source_url ?? sizes?.medium?.source_url ?? media.source_url : "";
2790 const tile = buildTileFromSpec({
2791 type: "attachment",
2792 ref: String(media.id),
2793 label: titleText,
2794 thumbnail: thumbUrl || void 0,
2795 icon: thumbUrl ? void 0 : dashiconForMime(media.mime_type),
2796 role: "entry",
2797 dataset: { mediaId: media.id, mime: media.mime_type },
2798 extraClasses: [
2799 "desktop-mode-my-wordpress__media-tile",
2800 "desktop-mode-my-wordpress__tile",
2801 "desktop-mode-my-wordpress__tile--media"
2802 ]
2803 });
2804 attachTileDragOut(tile, {
2805 kind: "attachment",
2806 ref: String(media.id),
2807 title: titleText,
2808 icon: dashiconForMime(media.mime_type),
2809 // Cross-frame bridge payload — lets the Gutenberg drop-
2810 // receiver build a `core/image` / `core/video` / `core/audio`
2811 // / `core/file` block when this tile is dropped on an open
2812 // editor iframe. The full-size source URL is the right block
2813 // attribute regardless of mime; the receiver picks the
2814 // concrete block from the MIME prefix.
2815 bridgePayload: {
2816 kind: "attachment",
2817 id: media.id,
2818 url: media.source_url,
2819 title: titleText,
2820 alt: stripTags(media.alt_text ?? ""),
2821 mime: media.mime_type,
2822 thumbnailUrl: thumbUrl || void 0,
2823 sizes: media.media_details?.sizes
2824 }
2825 });
2826 tile.addEventListener("click", () => selectTile$1(ctx, tile, media));
2827 tile.addEventListener("dblclick", (e) => {
2828 e.preventDefault();
2829 ctx.host.navigate({
2830 kind: "media-detail",
2831 entityId: ctx.entity.id,
2832 mediaId: media.id,
2833 mediaTitle: titleText
2834 });
2835 });
2836 tile.addEventListener("contextmenu", (e) => {
2837 e.preventDefault();
2838 openMediaTileMenu(ctx, tile, media, titleText, {
2839 x: e.clientX,
2840 y: e.clientY
2841 });
2842 });
2843 return tile;
2844 }
2845 function openMediaTileMenu(ctx, tile, media, titleText, pos) {
2846 closeAnyMediaTileMenu();
2847 selectTile$1(ctx, tile, media);
2848 const menu = document.createElement("wpd-context-menu");
2849 menu.setAttribute("open", "");
2850 menu.classList.add("desktop-mode-my-wordpress__menu");
2851 menu.style.left = `${pos.x}px`;
2852 menu.style.top = `${pos.y}px`;
2853 const base = [
2854 {
2855 id: "navigate-into",
2856 label: __("Navigate into", "desktop-mode"),
2857 icon: "dashicons-category"
2858 },
2859 {
2860 id: "open-source",
2861 label: __("Open file in new tab", "desktop-mode"),
2862 icon: "dashicons-external"
2863 },
2864 {
2865 id: "delete",
2866 label: __("Delete permanently", "desktop-mode"),
2867 icon: "dashicons-trash",
2868 danger: true
2869 }
2870 ];
2871 const filterCtx = {
2872 entityId: ctx.entity.id,
2873 kind: "attachment",
2874 item: media
2875 };
2876 const options = applyFilters(
2877 "desktop-mode.my-wordpress.tile-context-menu",
2878 base,
2879 filterCtx
2880 );
2881 const finalOptions = Array.isArray(options) ? options : base;
2882 for (const o of finalOptions) {
2883 const opt = document.createElement("wpd-context-menu-option");
2884 opt.dataset.menuItemId = o.id;
2885 opt.setAttribute("value", o.id);
2886 opt.setAttribute("icon", o.icon);
2887 if (o.danger) {
2888 opt.setAttribute("danger", "");
2889 }
2890 opt.textContent = o.label;
2891 menu.appendChild(opt);
2892 }
2893 menu.addEventListener("wpd-context-menu-pick", (e) => {
2894 const detail = e.detail;
2895 closeAnyMediaTileMenu();
2896 if (detail.id === "navigate-into") {
2897 ctx.host.navigate({
2898 kind: "media-detail",
2899 entityId: ctx.entity.id,
2900 mediaId: media.id,
2901 mediaTitle: titleText
2902 });
2903 return;
2904 }
2905 if (detail.id === "open-source") {
2906 window.open(media.source_url, "_blank", "noopener,noreferrer");
2907 return;
2908 }
2909 if (detail.id === "delete") {
2910 void confirmDeleteMedia(ctx, tile, media, titleText);
2911 return;
2912 }
2913 const match = finalOptions.find((o) => o.id === detail.id);
2914 if (match && typeof match.onSelect === "function") {
2915 try {
2916 match.onSelect();
2917 } catch (err) {
2918 console.error(
2919 `[my-wordpress/media] tile-context-menu '${detail.id}' onSelect threw:`,
2920 err
2921 );
2922 }
2923 }
2924 });
2925 document.body.appendChild(menu);
2926 const rect = menu.getBoundingClientRect();
2927 if (rect.right > window.innerWidth) {
2928 menu.style.left = `${Math.max(
2929 0,
2930 window.innerWidth - rect.width - 8
2931 )}px`;
2932 }
2933 if (rect.bottom > window.innerHeight) {
2934 menu.style.top = `${Math.max(
2935 0,
2936 window.innerHeight - rect.height - 8
2937 )}px`;
2938 }
2939 queueMicrotask(() => {
2940 const onDocPointerDown = (ev) => {
2941 if (ev.target instanceof Node && menu.contains(ev.target)) {
2942 return;
2943 }
2944 closeAnyMediaTileMenu();
2945 };
2946 const onDocKey = (ev) => {
2947 if (ev.key === "Escape") {
2948 closeAnyMediaTileMenu();
2949 }
2950 };
2951 document.addEventListener("pointerdown", onDocPointerDown, true);
2952 document.addEventListener("keydown", onDocKey);
2953 menu.addEventListener("tile-menu-closed", () => {
2954 document.removeEventListener(
2955 "pointerdown",
2956 onDocPointerDown,
2957 true
2958 );
2959 document.removeEventListener("keydown", onDocKey);
2960 });
2961 });
2962 }
2963 function closeAnyMediaTileMenu() {
2964 document.querySelectorAll("wpd-context-menu.desktop-mode-my-wordpress__menu").forEach((n) => {
2965 n.dispatchEvent(new CustomEvent("tile-menu-closed"));
2966 n.remove();
2967 });
2968 }
2969 async function confirmDeleteMedia(ctx, tile, media, titleText) {
2970 const ok = await wpdConfirm({
2971 title: __("Delete media?", "desktop-mode"),
2972 message: sprintf(
2973 // translators: %s is a media item title.
2974 __("“%s” will be permanently deleted. This cannot be undone.", "desktop-mode"),
2975 titleText
2976 ),
2977 confirmLabel: __("Delete", "desktop-mode"),
2978 cancelLabel: __("Cancel", "desktop-mode")
2979 });
2980 if (!ok) {
2981 return;
2982 }
2983 try {
2984 await deleteMediaItem(media.id);
2985 removeMediaFromList(ctx, tile, media.id);
2986 showToast$1({ message: __("Media deleted.", "desktop-mode") });
2987 } catch (err) {
2988 const message = err instanceof Error ? err.message : __("Couldn’t delete that file.", "desktop-mode");
2989 showToast$1({ message });
2990 }
2991 }
2992 function removeMediaFromList(ctx, tile, mediaId) {
2993 tile.remove();
2994 if (ctx.selectedId === mediaId) {
2995 ctx.selectedId = null;
2996 ctx.selectedTile = null;
2997 ctx.preview.replaceChildren();
2998 const placeholder = document.createElement("div");
2999 placeholder.className = "desktop-mode-my-wordpress__preview-empty";
3000 placeholder.textContent = __(
3001 "Select a media item to preview it here.",
3002 "desktop-mode"
3003 );
3004 ctx.preview.appendChild(placeholder);
3005 }
3006 ctx.loaded = Math.max(0, ctx.loaded - 1);
3007 ctx.total = Math.max(0, ctx.total - 1);
3008 paintStatus$2(ctx);
3009 }
3010 function selectTile$1(ctx, tile, media) {
3011 if (ctx.selectedTile) {
3012 ctx.selectedTile.removeAttribute("selected");
3013 }
3014 tile.setAttribute("selected", "");
3015 ctx.selectedTile = tile;
3016 ctx.selectedId = media.id;
3017 const titleText = stripTags(media.title.rendered) || __("(no title)", "desktop-mode");
3018 renderMediaPreview(ctx.preview, media, {
3019 entityId: ctx.entity.id,
3020 previewActions: ctx.previewActions,
3021 onOpenDetail: () => {
3022 ctx.host.navigate({
3023 kind: "media-detail",
3024 entityId: ctx.entity.id,
3025 mediaId: media.id,
3026 mediaTitle: titleText
3027 });
3028 }
3029 });
3030 }
3031 function renderEmpty(host, message) {
3032 const empty = document.createElement("div");
3033 empty.className = "desktop-mode-my-wordpress__empty";
3034 empty.textContent = message;
3035 host.appendChild(empty);
3036 }
3037 function renderMediaList(host, entity) {
3038 const cfg = getConfig();
3039 const initialQuery = lastQueryByMediaEntity.get(entity.id) ?? "";
3040 const toolbar = renderListToolbar({
3041 placeholder: __("Search media…", "desktop-mode"),
3042 ariaLabel: __("Search media", "desktop-mode"),
3043 initialValue: initialQuery,
3044 onSearchChange: (q) => {
3045 lastQueryByMediaEntity.set(entity.id, q);
3046 void resetForSearch(q);
3047 }
3048 });
3049 host.body.appendChild(toolbar.host);
3050 host.addTeardown(() => toolbar.destroy());
3051 const split = document.createElement("div");
3052 split.className = "desktop-mode-my-wordpress__split desktop-mode-my-wordpress__split--media";
3053 const left = document.createElement("div");
3054 left.className = "desktop-mode-my-wordpress__list";
3055 const tiles = document.createElement("div");
3056 tiles.className = "desktop-mode-my-wordpress__media-grid";
3057 tiles.setAttribute("role", "list");
3058 left.appendChild(tiles);
3059 const sentinel = document.createElement("div");
3060 sentinel.className = "desktop-mode-my-wordpress__sentinel";
3061 sentinel.setAttribute("aria-hidden", "true");
3062 left.appendChild(sentinel);
3063 const right = document.createElement("div");
3064 right.className = "desktop-mode-my-wordpress__preview";
3065 const previewEmpty = document.createElement("div");
3066 previewEmpty.className = "desktop-mode-my-wordpress__preview-empty";
3067 previewEmpty.textContent = __(
3068 "Select a media item to preview it here.",
3069 "desktop-mode"
3070 );
3071 right.appendChild(previewEmpty);
3072 split.append(left, right);
3073 host.body.appendChild(split);
3074 const statusBar = host.body.closest("[data-desktop-mode-my-wordpress-root]")?.querySelector(
3075 "[data-desktop-mode-my-wordpress-status]"
3076 ) ?? document.createElement("div");
3077 const ctx = {
3078 page: 0,
3079 totalPages: 1,
3080 total: 0,
3081 loaded: 0,
3082 loading: false,
3083 done: false,
3084 tiles,
3085 sentinel,
3086 preview: right,
3087 selectedId: null,
3088 selectedTile: null,
3089 statusBar,
3090 entity,
3091 host,
3092 previewActions: cfg.previewActions ?? [],
3093 query: initialQuery,
3094 abort: null
3095 };
3096 host.addTeardown(() => ctx.abort?.abort());
3097 const sentinelIsVisible = () => {
3098 const sr = sentinel.getBoundingClientRect();
3099 const rr = left.getBoundingClientRect();
3100 const slack = 200;
3101 return sr.top < rr.bottom + slack && sr.bottom > rr.top - slack;
3102 };
3103 const loadMore = async () => {
3104 if (ctx.loading || ctx.done) {
3105 return;
3106 }
3107 ctx.loading = true;
3108 const nextPage = ctx.page + 1;
3109 const perPage = cfg.mediaPerPage ?? 48;
3110 const queryAtFetchTime = ctx.query;
3111 const controller = new AbortController();
3112 ctx.abort = controller;
3113 try {
3114 const result = await fetchMediaPage(entity, {
3115 page: nextPage,
3116 perPage,
3117 search: queryAtFetchTime || void 0,
3118 signal: controller.signal
3119 });
3120 if (ctx.query !== queryAtFetchTime) {
3121 return;
3122 }
3123 ctx.page = nextPage;
3124 ctx.totalPages = result.totalPages;
3125 ctx.total = result.total;
3126 if (result.items.length === 0 && nextPage === 1) {
3127 renderEmpty(
3128 tiles,
3129 queryAtFetchTime ? sprintf(
3130 // translators: %s is the user-entered search query.
3131 __('No media match "%s".', "desktop-mode"),
3132 queryAtFetchTime
3133 ) : __("No media yet.", "desktop-mode")
3134 );
3135 ctx.done = true;
3136 paintStatus$2(ctx);
3137 return;
3138 }
3139 for (const item of result.items) {
3140 tiles.appendChild(buildMediaTile(ctx, item));
3141 ctx.loaded += 1;
3142 }
3143 if (ctx.page >= ctx.totalPages) {
3144 ctx.done = true;
3145 }
3146 paintStatus$2(ctx);
3147 } catch (err) {
3148 if (err instanceof DOMException && err.name === "AbortError") {
3149 return;
3150 }
3151 const message = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
3152 renderEmpty(tiles, message);
3153 ctx.done = true;
3154 } finally {
3155 ctx.loading = false;
3156 if (ctx.abort === controller) {
3157 ctx.abort = null;
3158 }
3159 }
3160 if (!ctx.done) {
3161 requestAnimationFrame(() => {
3162 if (sentinelIsVisible()) {
3163 void loadMore();
3164 }
3165 });
3166 }
3167 };
3168 const resetForSearch = async (q) => {
3169 ctx.abort?.abort();
3170 ctx.abort = null;
3171 ctx.query = q;
3172 tiles.classList.add(
3173 "desktop-mode-my-wordpress__media-grid--searching"
3174 );
3175 const controller = new AbortController();
3176 ctx.abort = controller;
3177 ctx.loading = true;
3178 const perPage = cfg.mediaPerPage ?? 48;
3179 try {
3180 const result = await fetchMediaPage(entity, {
3181 page: 1,
3182 perPage,
3183 search: q || void 0,
3184 signal: controller.signal
3185 });
3186 if (ctx.query !== q) {
3187 return;
3188 }
3189 tiles.replaceChildren();
3190 tiles.classList.remove(
3191 "desktop-mode-my-wordpress__media-grid--searching"
3192 );
3193 ctx.page = 1;
3194 ctx.totalPages = result.totalPages;
3195 ctx.total = result.total;
3196 ctx.loaded = 0;
3197 ctx.done = ctx.page >= ctx.totalPages;
3198 ctx.selectedId = null;
3199 ctx.selectedTile = null;
3200 ctx.preview.replaceChildren();
3201 const emptyPreview = document.createElement("div");
3202 emptyPreview.className = "desktop-mode-my-wordpress__preview-empty";
3203 emptyPreview.textContent = __(
3204 "Select a media item to preview it here.",
3205 "desktop-mode"
3206 );
3207 ctx.preview.appendChild(emptyPreview);
3208 if (result.items.length === 0) {
3209 renderEmpty(
3210 tiles,
3211 q ? sprintf(
3212 // translators: %s is the user-entered search query.
3213 __('No media match "%s".', "desktop-mode"),
3214 q
3215 ) : __("No media yet.", "desktop-mode")
3216 );
3217 ctx.done = true;
3218 } else {
3219 for (const item of result.items) {
3220 tiles.appendChild(buildMediaTile(ctx, item));
3221 ctx.loaded += 1;
3222 }
3223 }
3224 paintStatus$2(ctx);
3225 } catch (err) {
3226 if (err instanceof DOMException && err.name === "AbortError") {
3227 return;
3228 }
3229 tiles.classList.remove(
3230 "desktop-mode-my-wordpress__media-grid--searching"
3231 );
3232 tiles.replaceChildren();
3233 const message = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
3234 renderEmpty(tiles, message);
3235 ctx.done = true;
3236 } finally {
3237 ctx.loading = false;
3238 if (ctx.abort === controller) {
3239 ctx.abort = null;
3240 }
3241 }
3242 if (!ctx.done) {
3243 requestAnimationFrame(() => {
3244 if (sentinelIsVisible()) {
3245 void loadMore();
3246 }
3247 });
3248 }
3249 };
3250 if (typeof IntersectionObserver !== "undefined") {
3251 const observer = new IntersectionObserver(
3252 (entries) => {
3253 for (const e of entries) {
3254 if (e.isIntersecting) {
3255 void loadMore();
3256 }
3257 }
3258 },
3259 { root: left, rootMargin: "200px 0px" }
3260 );
3261 observer.observe(sentinel);
3262 host.addTeardown(() => observer.disconnect());
3263 } else {
3264 const onScroll = () => {
3265 if (sentinelIsVisible()) {
3266 void loadMore();
3267 }
3268 };
3269 left.addEventListener("scroll", onScroll, { passive: true });
3270 host.addTeardown(() => left.removeEventListener("scroll", onScroll));
3271 }
3272 const liveNs = `desktop-mode/my-wordpress-media-live-${Math.random().toString(36).slice(2, 8)}`;
3273 addAction(FILE_DROP_HOOKS.AFTER_UPLOAD, liveNs, (payload) => {
3274 void spliceNewMedia(ctx, payload.result.id);
3275 });
3276 host.addTeardown(
3277 () => removeAction(FILE_DROP_HOOKS.AFTER_UPLOAD, liveNs)
3278 );
3279 host.addTeardown(() => closeAnyMediaTileMenu());
3280 paintStatus$2(ctx);
3281 void loadMore();
3282 }
3283 async function spliceNewMedia(ctx, mediaId) {
3284 if (!ctx.tiles.isConnected) {
3285 return;
3286 }
3287 if (ctx.tiles.querySelector(
3288 `wpd-tile[data-media-id="${mediaId}"]`
3289 )) {
3290 return;
3291 }
3292 try {
3293 const item = await fetchMediaItem(mediaId);
3294 ctx.tiles.insertBefore(buildMediaTile(ctx, item), ctx.tiles.firstChild);
3295 ctx.loaded += 1;
3296 ctx.total += 1;
3297 paintStatus$2(ctx);
3298 } catch (err) {
3299 console.warn(
3300 "[my-wordpress/media] live-refresh fetch failed:",
3301 err
3302 );
3303 }
3304 }
3305 function entityForPostType(postType) {
3306 const suffix = "/" + postType;
3307 return getConfig().entities.find((e) => e.restPath.endsWith(suffix));
3308 }
3309 function entityIdForPostType(postType) {
3310 const match = entityForPostType(postType);
3311 if (match) {
3312 return match.id;
3313 }
3314 if (postType === "page") {
3315 return "pages";
3316 }
3317 return "posts";
3318 }
3319 function entityIconForPostType(postType) {
3320 const match = entityForPostType(postType);
3321 if (match) {
3322 return match.icon;
3323 }
3324 if (postType === "page") {
3325 return "dashicons-admin-page";
3326 }
3327 return "dashicons-admin-post";
3328 }
3329 function openDetailInWindow(payload) {
3330 const myWp = window.wp?.desktop?.myWordpress;
3331 myWp?.openDetail?.(payload);
3332 }
3333 function buildUsageTile(row) {
3334 const titleText = row.title || `#${row.postId}`;
3335 const tile = buildTileFromSpec({
3336 type: "post",
3337 ref: String(row.postId),
3338 label: titleText,
3339 icon: entityIconForPostType(row.postType),
3340 role: "entry",
3341 status: row.status,
3342 dataset: { postId: row.postId, postType: row.postType },
3343 extraClasses: [
3344 "desktop-mode-my-wordpress__tile",
3345 "desktop-mode-my-wordpress__tile--entry",
3346 "desktop-mode-my-wordpress__media-tile",
3347 "desktop-mode-my-wordpress__tile--usage"
3348 ]
3349 });
3350 attachTileDragOut(tile, {
3351 kind: "post",
3352 ref: String(row.postId),
3353 title: titleText,
3354 icon: entityIconForPostType(row.postType)
3355 });
3356 return tile;
3357 }
3358 let openContextMenu = null;
3359 function closeContextMenu() {
3360 if (openContextMenu && openContextMenu.isConnected) {
3361 openContextMenu.remove();
3362 }
3363 openContextMenu = null;
3364 }
3365 function openUsageTileMenu(row, pos) {
3366 closeContextMenu();
3367 const menu = document.createElement("wpd-context-menu");
3368 menu.setAttribute("open", "");
3369 menu.classList.add("desktop-mode-my-wordpress__menu");
3370 menu.style.left = `${pos.x}px`;
3371 menu.style.top = `${pos.y}px`;
3372 const addOption = (id, label, icon) => {
3373 const opt = document.createElement("wpd-context-menu-option");
3374 opt.dataset.menuItemId = id;
3375 opt.setAttribute("value", id);
3376 opt.setAttribute("icon", icon);
3377 opt.textContent = label;
3378 menu.appendChild(opt);
3379 };
3380 addOption("navigate-into", __("Open in My WordPress", "desktop-mode"), "dashicons-category");
3381 if (row.editLink) {
3382 addOption("open-editor", __("Open in editor", "desktop-mode"), "dashicons-edit");
3383 }
3384 if (row.link) {
3385 addOption("open-front", __("View on site", "desktop-mode"), "dashicons-external");
3386 }
3387 menu.addEventListener("wpd-context-menu-pick", (e) => {
3388 const detail = e.detail;
3389 closeContextMenu();
3390 if (detail.id === "navigate-into") {
3391 openDetailInWindow({
3392 entityId: entityIdForPostType(row.postType),
3393 postId: row.postId,
3394 postTitle: row.title
3395 });
3396 return;
3397 }
3398 if (detail.id === "open-editor" && row.editLink) {
3399 window.open(row.editLink, "_blank", "noopener,noreferrer");
3400 return;
3401 }
3402 if (detail.id === "open-front" && row.link) {
3403 window.open(row.link, "_blank", "noopener,noreferrer");
3404 }
3405 });
3406 document.body.appendChild(menu);
3407 openContextMenu = menu;
3408 const rect = menu.getBoundingClientRect();
3409 if (rect.right > window.innerWidth) {
3410 menu.style.left = `${Math.max(
3411 0,
3412 window.innerWidth - rect.width - 8
3413 )}px`;
3414 }
3415 if (rect.bottom > window.innerHeight) {
3416 menu.style.top = `${Math.max(
3417 0,
3418 window.innerHeight - rect.height - 8
3419 )}px`;
3420 }
3421 queueMicrotask(() => {
3422 const onDoc = (ev) => {
3423 const target = ev.target;
3424 if (target instanceof Node && menu.contains(target)) {
3425 return;
3426 }
3427 closeContextMenu();
3428 document.removeEventListener("pointerdown", onDoc, true);
3429 document.removeEventListener("keydown", onKey);
3430 };
3431 const onKey = (ev) => {
3432 if (ev.key === "Escape") {
3433 closeContextMenu();
3434 document.removeEventListener("pointerdown", onDoc, true);
3435 document.removeEventListener("keydown", onKey);
3436 }
3437 };
3438 document.addEventListener("pointerdown", onDoc, true);
3439 document.addEventListener("keydown", onKey);
3440 });
3441 }
3442 function paintStatus$1(statusBar, count, entityId) {
3443 const segments = [
3444 {
3445 id: "count",
3446 label: sprintf(
3447 // translators: %d is the count of posts that reference an attachment.
3448 _n("%d reference", "%d references", count),
3449 count
3450 ),
3451 align: "start",
3452 sort: 10
3453 }
3454 ];
3455 const filtered = applyFilters(
3456 "desktop-mode.my-wordpress.status-bar",
3457 segments,
3458 { view: "media-detail", entityId }
3459 );
3460 renderStatusBarSegments(
3461 statusBar,
3462 Array.isArray(filtered) ? filtered : segments
3463 );
3464 }
3465 async function renderMediaDetail(host, mediaId) {
3466 const wrap = document.createElement("div");
3467 wrap.className = "desktop-mode-my-wordpress__split desktop-mode-my-wordpress__split--media-detail";
3468 const left = document.createElement("div");
3469 left.className = "desktop-mode-my-wordpress__list desktop-mode-my-wordpress__usage-list";
3470 const loading = document.createElement("div");
3471 loading.className = "desktop-mode-my-wordpress__preview-loading";
3472 const spinner = document.createElement("wpd-spinner");
3473 loading.appendChild(spinner);
3474 left.appendChild(loading);
3475 const right = document.createElement("div");
3476 right.className = "desktop-mode-my-wordpress__preview";
3477 wrap.append(left, right);
3478 host.body.appendChild(wrap);
3479 const statusBar = host.body.closest("[data-desktop-mode-my-wordpress-root]")?.querySelector(
3480 "[data-desktop-mode-my-wordpress-status]"
3481 ) ?? document.createElement("div");
3482 let usage;
3483 try {
3484 usage = await fetchMediaUsage(mediaId);
3485 } catch (err) {
3486 if (!wrap.isConnected) {
3487 return;
3488 }
3489 left.replaceChildren();
3490 const errBox = document.createElement("div");
3491 errBox.className = "desktop-mode-my-wordpress__error";
3492 errBox.textContent = err instanceof Error ? err.message : __("Failed to load usage data.", "desktop-mode");
3493 left.appendChild(errBox);
3494 return;
3495 }
3496 if (!wrap.isConnected) {
3497 return;
3498 }
3499 if (host.route.kind !== "media-detail") {
3500 throw new Error(
3501 "[my-wordpress] renderMediaDetail invoked outside a media-detail route."
3502 );
3503 }
3504 const entityId = host.route.entityId;
3505 const summary = document.createElement("div");
3506 summary.className = "desktop-mode-my-wordpress__media-detail-summary-bar";
3507 const summaryText = document.createElement("p");
3508 summaryText.className = "desktop-mode-my-wordpress__media-detail-summary";
3509 summaryText.textContent = sprintf(
3510 // translators: %d is the count of posts/pages referencing this file.
3511 _n(
3512 "%d entry references this file.",
3513 "%d entries reference this file.",
3514 usage.usedIn.length
3515 ),
3516 usage.usedIn.length
3517 );
3518 summary.appendChild(summaryText);
3519 right.replaceChildren(summary);
3520 const mediaItem = {
3521 id: usage.media.id,
3522 title: { rendered: usage.media.title },
3523 date: usage.media.date,
3524 mime_type: usage.media.mime,
3525 source_url: usage.media.sourceUrl,
3526 media_details: {
3527 file: usage.media.filename
3528 },
3529 _embedded: usage.media.author.name ? { author: [{ id: usage.media.author.id, name: usage.media.author.name }] } : void 0
3530 };
3531 const previewHost = document.createElement("div");
3532 previewHost.className = "desktop-mode-my-wordpress__media-detail-preview";
3533 renderMediaPreview(previewHost, mediaItem, {
3534 entityId,
3535 previewActions: getConfig().previewActions ?? []
3536 });
3537 right.appendChild(previewHost);
3538 left.replaceChildren();
3539 if (usage.usedIn.length === 0) {
3540 const empty = document.createElement("div");
3541 empty.className = "desktop-mode-my-wordpress__empty";
3542 empty.textContent = __(
3543 "No posts or pages reference this file.",
3544 "desktop-mode"
3545 );
3546 left.appendChild(empty);
3547 paintStatus$1(statusBar, 0, entityId);
3548 return;
3549 }
3550 const grid = document.createElement("div");
3551 grid.className = "desktop-mode-my-wordpress__media-grid desktop-mode-my-wordpress__usage-grid";
3552 grid.setAttribute("role", "list");
3553 for (const row of usage.usedIn) {
3554 const tile = buildUsageTile(row);
3555 tile.addEventListener("dblclick", (e) => {
3556 e.preventDefault();
3557 openDetailInWindow({
3558 entityId: entityIdForPostType(row.postType),
3559 postId: row.postId,
3560 postTitle: row.title
3561 });
3562 });
3563 tile.addEventListener("contextmenu", (e) => {
3564 e.preventDefault();
3565 openUsageTileMenu(row, { x: e.clientX, y: e.clientY });
3566 });
3567 grid.appendChild(tile);
3568 }
3569 left.appendChild(grid);
3570 host.addTeardown(closeContextMenu);
3571 paintStatus$1(statusBar, usage.usedIn.length, entityId);
3572 }
3573 const ROOT_CLASS = "desktop-mode-breadcrumbs";
3574 function renderBreadcrumbs(host, segments, opts = {}) {
3575 host.replaceChildren();
3576 host.classList.add(ROOT_CLASS);
3577 if (opts.onBack) {
3578 const back = document.createElement("button");
3579 back.type = "button";
3580 back.className = `${ROOT_CLASS}__back`;
3581 back.setAttribute("aria-label", __("Back", "desktop-mode"));
3582 back.title = __("Back", "desktop-mode");
3583 const arrow = document.createElement("span");
3584 arrow.className = "dashicons dashicons-arrow-left-alt2";
3585 arrow.setAttribute("aria-hidden", "true");
3586 back.appendChild(arrow);
3587 if (opts.backDisabled) {
3588 back.disabled = true;
3589 }
3590 const onBack = opts.onBack;
3591 back.addEventListener("click", () => {
3592 if (back.disabled) {
3593 return;
3594 }
3595 onBack();
3596 });
3597 host.appendChild(back);
3598 }
3599 const nav = document.createElement("nav");
3600 nav.className = `${ROOT_CLASS}__crumbs`;
3601 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
3602 segments.forEach((seg, idx) => {
3603 if (idx > 0) {
3604 const sep = document.createElement("span");
3605 sep.className = `${ROOT_CLASS}__sep`;
3606 sep.setAttribute("aria-hidden", "true");
3607 sep.textContent = "";
3608 nav.appendChild(sep);
3609 }
3610 if (!seg.onClick) {
3611 const here = document.createElement("span");
3612 here.className = `${ROOT_CLASS}__crumb ${ROOT_CLASS}__crumb--current`;
3613 here.setAttribute("aria-current", "page");
3614 here.textContent = seg.label;
3615 nav.appendChild(here);
3616 return;
3617 }
3618 const btn = document.createElement("button");
3619 btn.type = "button";
3620 btn.className = `${ROOT_CLASS}__crumb`;
3621 btn.textContent = seg.label;
3622 const onClick = seg.onClick;
3623 btn.addEventListener("click", () => {
3624 onClick();
3625 });
3626 nav.appendChild(btn);
3627 });
3628 host.appendChild(nav);
3629 }
3630 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}`;
3631 const _WpdButton = class _WpdButton extends Component {
3632 render() {
3633 const disabled = this.disabled !== null;
3634 const type = this.type || "button";
3635 return html`
3636 <button part="button" type=${type} ?disabled=${disabled}>
3637 <slot></slot>
3638 </button>
3639 `;
3640 }
3641 };
3642 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
3643 _WpdButton.styles = [styles$1];
3644 _WpdButton.help = {
3645 title: "Button",
3646 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
3647 status: "stable",
3648 since: "0.9.0",
3649 props: [
3650 {
3651 name: "variant",
3652 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
3653 default: "ghost",
3654 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
3655 },
3656 {
3657 name: "disabled",
3658 type: "boolean attribute",
3659 description: "Disable pointer + keyboard interaction and dim the chrome."
3660 },
3661 {
3662 name: "type",
3663 type: "'button' | 'submit' | 'reset'",
3664 default: "button",
3665 description: "Forwarded to the underlying native <button>."
3666 },
3667 {
3668 name: "busy",
3669 type: "boolean attribute",
3670 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
3671 },
3672 {
3673 name: "fill-cell",
3674 type: "boolean attribute",
3675 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
3676 }
3677 ],
3678 slots: [{ name: "(default)", description: "Button label." }],
3679 parts: [{ name: "button", description: "Underlying <button> element." }],
3680 cssProps: [
3681 { name: "--wpd-button-bg", description: "Background color." },
3682 { name: "--wpd-button-fg", description: "Text color." },
3683 { name: "--wpd-button-border", description: "Border shorthand." },
3684 { name: "--wpd-button-border-radius", default: "6px" },
3685 { name: "--wpd-button-padding", default: "6px 12px" },
3686 {
3687 name: "--wpd-button-min-height",
3688 description: "Minimum height when fill-cell is set."
3689 }
3690 ],
3691 example: html`
3692 <wpd-cluster gap="8">
3693 <wpd-button variant="primary">Primary</wpd-button>
3694 <wpd-button variant="secondary">Secondary</wpd-button>
3695 <wpd-button variant="ghost">Ghost</wpd-button>
3696 <wpd-button variant="danger">Danger</wpd-button>
3697 <wpd-button variant="link">Link</wpd-button>
3698 </wpd-cluster>
3699 `
3700 };
3701 let WpdButton = _WpdButton;
3702 defineComponent("wpd-button", WpdButton);
3703 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}`;
3704 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}`;
3705 const _WpdContextMenu = class _WpdContextMenu extends Component {
3706 render() {
3707 return html`
3708 <slot></slot>
3709 `;
3710 }
3711 connectedCallback() {
3712 super.connectedCallback();
3713 this.setAttribute("role", "menu");
3714 }
3715 };
3716 _WpdContextMenu.props = ["open"];
3717 _WpdContextMenu.styles = [menuStyles];
3718 _WpdContextMenu.help = {
3719 title: "Context menu",
3720 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.",
3721 status: "experimental",
3722 since: "0.9.0",
3723 props: [
3724 {
3725 name: "open",
3726 type: "boolean attribute",
3727 description: "Mounts the menu in its open / visible state."
3728 }
3729 ],
3730 slots: [
3731 { name: "(default)", description: "List of <wpd-context-menu-option> items." }
3732 ],
3733 events: [
3734 {
3735 name: "wpd-context-menu-pick",
3736 description: "Bubbled from a non-disabled, non-heading option on activation. Detail: `{ id, value }`."
3737 }
3738 ]
3739 };
3740 let WpdContextMenu = _WpdContextMenu;
3741 defineComponent("wpd-context-menu", WpdContextMenu);
3742 const _WpdContextMenuOption = class _WpdContextMenuOption extends Component {
3743 constructor() {
3744 super(...arguments);
3745 this._onActivate = (e) => {
3746 if (this.hasAttribute("disabled") || this.hasAttribute("heading")) {
3747 return;
3748 }
3749 const target = e.target;
3750 if (target && target !== this && target.closest("wpd-context-menu-option") !== this) {
3751 return;
3752 }
3753 this.emit("wpd-context-menu-pick", {
3754 id: this.dataset.menuItemId ?? this.id ?? "",
3755 value: this.getAttribute("value") ?? ""
3756 });
3757 };
3758 this._onKey = (e) => {
3759 if (e.key === "Enter" || e.key === " ") {
3760 e.preventDefault();
3761 this._onActivate(e);
3762 }
3763 };
3764 }
3765 connectedCallback() {
3766 super.connectedCallback();
3767 const isHeading = this.hasAttribute("heading");
3768 this.setAttribute("role", isHeading ? "presentation" : "menuitem");
3769 if (!isHeading) {
3770 this.setAttribute("tabindex", "0");
3771 }
3772 this.addEventListener("click", this._onActivate);
3773 this.addEventListener("keydown", this._onKey);
3774 }
3775 disconnectedCallback() {
3776 this.removeEventListener("click", this._onActivate);
3777 this.removeEventListener("keydown", this._onKey);
3778 }
3779 render() {
3780 const icon = this.getAttribute("icon");
3781 const hasChildren2 = this.hasAttribute("has-children");
3782 const checked = this.hasAttribute("checked");
3783 return html`
3784 ${checked ? html`<span class="check" aria-hidden="true">✓</span>` : html``}
3785 ${icon ? html`<span class="icon dashicons ${icon}" aria-hidden="true"></span>` : html``}
3786 <span class="label"><slot></slot></span>
3787 ${hasChildren2 ? html`<span class="chevron" aria-hidden="true">›</span>` : html``}
3788 `;
3789 }
3790 };
3791 _WpdContextMenuOption.props = [
3792 "value",
3793 "icon",
3794 "disabled",
3795 "danger",
3796 "heading",
3797 "has-children",
3798 "checked"
3799 ];
3800 _WpdContextMenuOption.styles = [optionStyles];
3801 _WpdContextMenuOption.help = {
3802 title: "Context menu option",
3803 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.",
3804 status: "experimental",
3805 since: "0.9.0",
3806 props: [
3807 {
3808 name: "value",
3809 type: "string",
3810 description: "Forwarded as `detail.value` on activation."
3811 },
3812 {
3813 name: "icon",
3814 type: "string",
3815 description: "Dashicon class (e.g. `dashicons-trash`)."
3816 },
3817 {
3818 name: "disabled",
3819 type: "boolean attribute",
3820 description: "Renders the option dimmed; clicks are ignored."
3821 },
3822 {
3823 name: "danger",
3824 type: "boolean attribute",
3825 description: "Destructive styling — red text, red hover."
3826 },
3827 {
3828 name: "heading",
3829 type: "boolean attribute",
3830 description: "Non-interactive section header. Ignores clicks."
3831 },
3832 {
3833 name: "has-children",
3834 type: "boolean attribute",
3835 description: "Renders a trailing chevron to suggest a submenu."
3836 },
3837 {
3838 name: "checked",
3839 type: "boolean attribute",
3840 description: "Renders a leading check mark — for radio-style picks inside a submenu (e.g. the active Sort By order)."
3841 }
3842 ],
3843 slots: [
3844 { name: "(default)", description: "Visible label + optional nested <wpd-context-menu>." }
3845 ],
3846 events: [
3847 {
3848 name: "wpd-context-menu-pick",
3849 description: "Bubbled on click / Enter for non-heading non-disabled options. Detail: `{ id, value }`."
3850 }
3851 ]
3852 };
3853 let WpdContextMenuOption = _WpdContextMenuOption;
3854 defineComponent("wpd-context-menu-option", WpdContextMenuOption);
3855 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}}`;
3856 const WPD_SPINNER_PRESETS = Object.freeze({
3857 classic: {
3858 sp1: 12,
3859 sp2: 24,
3860 sp3: 40,
3861 a1: 28,
3862 a2: 15,
3863 a3: 8,
3864 gap: 4,
3865 dir2: 1,
3866 dir3: -1,
3867 pulse: "none",
3868 dots: 0
3869 },
3870 comet: {
3871 sp1: 8,
3872 sp2: 14,
3873 sp3: 26,
3874 a1: 50,
3875 a2: 28,
3876 a3: 12,
3877 gap: 3,
3878 dir2: 1,
3879 dir3: 1,
3880 pulse: "none",
3881 dots: 5
3882 },
3883 orbit: {
3884 sp1: 10,
3885 sp2: 10,
3886 sp3: 32,
3887 a1: 50,
3888 a2: 50,
3889 a3: 8,
3890 gap: 5,
3891 dir2: -1,
3892 dir3: -1,
3893 pulse: "opacity",
3894 dots: 3
3895 },
3896 pulse: {
3897 sp1: 6,
3898 sp2: 18,
3899 sp3: 30,
3900 a1: 20,
3901 a2: 12,
3902 a3: 6,
3903 gap: 4,
3904 dir2: 1,
3905 dir3: -1,
3906 pulse: "both",
3907 dots: 8
3908 }
3909 });
3910 const CX = 61.26;
3911 const CY = 61.26;
3912 const DISC_R = 58.453;
3913 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"/>';
3914 const _WpdSpinner = class _WpdSpinner extends Component {
3915 constructor() {
3916 super(...arguments);
3917 this._paintScheduled = false;
3918 }
3919 connectedCallback() {
3920 super.connectedCallback();
3921 this._schedulePaint();
3922 }
3923 render() {
3924 return html`<div class="root" part="root"></div>`;
3925 }
3926 requestUpdate() {
3927 super.requestUpdate();
3928 this._schedulePaint();
3929 }
3930 _schedulePaint() {
3931 if (this._paintScheduled || !this.isConnected) {
3932 return;
3933 }
3934 this._paintScheduled = true;
3935 queueMicrotask(() => {
3936 this._paintScheduled = false;
3937 if (!this.isConnected) {
3938 return;
3939 }
3940 this._paint();
3941 });
3942 }
3943 _paint() {
3944 this._syncCssVars();
3945 const root = this.shadowRoot?.querySelector(
3946 ".root"
3947 );
3948 if (!root) {
3949 return;
3950 }
3951 root.innerHTML = this._buildSvg();
3952 }
3953 /**
3954 * Reflect the color / accent / size attributes onto CSS custom
3955 * properties on the host. Removing the attribute clears the var
3956 * so the default cascades back in.
3957 */
3958 _syncCssVars() {
3959 const sync = (attr, varName, transform) => {
3960 const v = this.getAttribute(attr);
3961 if (v === null) {
3962 this.style.removeProperty(varName);
3963 } else {
3964 this.style.setProperty(
3965 varName,
3966 transform ? transform(v) : v
3967 );
3968 }
3969 };
3970 sync("color", "--wpd-spinner-color");
3971 sync("accent", "--wpd-spinner-accent");
3972 sync(
3973 "size",
3974 "--wpd-spinner-size",
3975 (v) => /^-?\d+(\.\d+)?$/.test(v.trim()) ? `${v}px` : v
3976 );
3977 }
3978 _effectiveConfig() {
3979 const presetName = this.getAttribute("preset") ?? "classic";
3980 const preset = WPD_SPINNER_PRESETS[presetName] ?? WPD_SPINNER_PRESETS.classic;
3981 const num = (attr, fallback) => {
3982 const v = this.getAttribute(attr);
3983 if (v === null) {
3984 return fallback;
3985 }
3986 const n = parseFloat(v);
3987 return Number.isFinite(n) ? n : fallback;
3988 };
3989 const dir = (attr, fallback) => {
3990 const v = this.getAttribute(attr);
3991 if (v === null) {
3992 return fallback;
3993 }
3994 const lc = v.toLowerCase();
3995 if (lc === "-1" || lc === "ccw" || lc === "reverse") {
3996 return -1;
3997 }
3998 return 1;
3999 };
4000 const pulse = () => {
4001 const v = this.getAttribute("pulse");
4002 if (v === "scale" || v === "opacity" || v === "both" || v === "none") {
4003 return v;
4004 }
4005 return preset.pulse;
4006 };
4007 return {
4008 sp1: num("sp1", preset.sp1),
4009 sp2: num("sp2", preset.sp2),
4010 sp3: num("sp3", preset.sp3),
4011 a1: num("a1", preset.a1),
4012 a2: num("a2", preset.a2),
4013 a3: num("a3", preset.a3),
4014 gap: num("gap", preset.gap),
4015 dir2: dir("dir2", preset.dir2),
4016 dir3: dir("dir3", preset.dir3),
4017 pulse: pulse(),
4018 dots: Math.max(0, Math.floor(num("dots", preset.dots)))
4019 };
4020 }
4021 _buildSvg() {
4022 const cfg = this._effectiveConfig();
4023 const label = escAttr(this.getAttribute("label") ?? "Loading");
4024 const pad = cfg.gap * 3 + 14;
4025 const vbMin = -pad;
4026 const vbSize = 122.52 + pad * 2;
4027 const r1 = DISC_R + cfg.gap + 2;
4028 const r2 = r1 + cfg.gap + 2;
4029 const r3 = r2 + cfg.gap + 1.5;
4030 const ring1Anim = `animation: wpd-spinner-spin ${(cfg.sp1 / 10).toFixed(2)}s linear infinite`;
4031 const ring2Anim = `animation: wpd-spinner-spin ${(cfg.sp2 / 10).toFixed(2)}s linear infinite${cfg.dir2 < 0 ? " reverse" : ""}`;
4032 const ring3Anim = `animation: wpd-spinner-spin ${(cfg.sp3 / 10).toFixed(2)}s linear infinite${cfg.dir3 < 0 ? " reverse" : ""}`;
4033 const pspd = (cfg.sp1 * 1.8 / 10).toFixed(1);
4034 const ospd = (cfg.sp1 * 2.3 / 10).toFixed(1);
4035 let pulseStyle = "";
4036 if (cfg.pulse === "scale") {
4037 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite`;
4038 } else if (cfg.pulse === "opacity") {
4039 pulseStyle = `animation: wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
4040 } else if (cfg.pulse === "both") {
4041 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite, wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
4042 }
4043 let dotEls = "";
4044 if (cfg.dots > 0) {
4045 const dr = r3 + cfg.gap + 1;
4046 const dc2 = 2 * Math.PI * dr;
4047 const dsz = 1.6;
4048 const dotDur = (cfg.sp1 * 0.65 / 10).toFixed(2);
4049 for (let i = 0; i < cfg.dots; i++) {
4050 const offset = -(i / cfg.dots) * dc2;
4051 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"/>`;
4052 }
4053 }
4054 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>`;
4055 }
4056 };
4057 _WpdSpinner.props = [
4058 "preset",
4059 "size",
4060 "color",
4061 "accent",
4062 "sp1",
4063 "sp2",
4064 "sp3",
4065 "a1",
4066 "a2",
4067 "a3",
4068 "gap",
4069 "dir2",
4070 "dir3",
4071 "pulse",
4072 "dots",
4073 "label"
4074 ];
4075 _WpdSpinner.styles = [styles];
4076 _WpdSpinner.help = {
4077 title: "Spinner",
4078 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.",
4079 status: "experimental",
4080 since: "0.18.0",
4081 props: [
4082 {
4083 name: "preset",
4084 type: '"classic" | "comet" | "orbit" | "pulse"',
4085 default: "classic",
4086 description: "Visual personality. Every other attribute defaults to the preset's value and can be overridden individually."
4087 },
4088 {
4089 name: "size",
4090 type: "integer (px) or CSS length",
4091 default: "48",
4092 description: "Sets `--wpd-spinner-size`. Bare numbers are treated as px; pass a CSS length (e.g. `2em`) to opt into ems / rems."
4093 },
4094 {
4095 name: "color",
4096 type: "CSS color",
4097 description: "Disc + ring + dot color. Sets `--wpd-spinner-color`. Default inherits the WP admin theme color."
4098 },
4099 {
4100 name: "accent",
4101 type: "CSS color",
4102 default: "#fff",
4103 description: "Color of the W mark inside the disc. Sets `--wpd-spinner-accent`. Default white — change for dark-on-light or themed marks."
4104 },
4105 {
4106 name: "sp1, sp2, sp3",
4107 type: "integer (deciseconds)",
4108 description: "Per-ring rotation duration in tenths-of-a-second (12 → 1.2s). Higher = slower."
4109 },
4110 {
4111 name: "a1, a2, a3",
4112 type: "integer (0-100)",
4113 description: "Per-ring arc length as a percentage of the ring circumference."
4114 },
4115 {
4116 name: "gap",
4117 type: "integer",
4118 description: "Gap between concentric rings (units approximate to px at 120-viewport)."
4119 },
4120 {
4121 name: "dir2, dir3",
4122 type: '"1" | "-1" | "cw" | "ccw"',
4123 description: "Per-ring direction; ring 1 is always clockwise."
4124 },
4125 {
4126 name: "pulse",
4127 type: '"none" | "scale" | "opacity" | "both"',
4128 description: "Pulse animation applied to the disc + W mark."
4129 },
4130 {
4131 name: "dots",
4132 type: "integer",
4133 description: "Outer trailing dot count. Sensible values: 0, 3, 5, 8."
4134 },
4135 {
4136 name: "label",
4137 type: "string",
4138 default: "Loading",
4139 description: 'Accessible name for the SVG (`role="img"` + `aria-label`).'
4140 }
4141 ],
4142 cssProps: [
4143 { name: "--wpd-spinner-color", default: "var(--wp-admin-theme-color, #21759b)" },
4144 { name: "--wpd-spinner-accent", default: "#fff" },
4145 { name: "--wpd-spinner-size", default: "48px" }
4146 ],
4147 example: html`<wpd-spinner preset="comet" size="80"></wpd-spinner>`
4148 };
4149 let WpdSpinner = _WpdSpinner;
4150 function dasharray(r, pct) {
4151 const c = 2 * Math.PI * r;
4152 const visible = pct / 100 * c;
4153 const gap = c - visible;
4154 return `${visible.toFixed(2)} ${gap.toFixed(2)}`;
4155 }
4156 function escAttr(s) {
4157 return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4158 }
4159 defineComponent("wpd-spinner", WpdSpinner);
4160 const WINDOW_ID = "desktop-mode-my-wordpress";
4161 const ROOT_SEL = "[data-desktop-mode-my-wordpress-root]";
4162 const BREADCRUMBS_SEL = "[data-desktop-mode-my-wordpress-breadcrumbs]";
4163 const BODY_SEL = "[data-desktop-mode-my-wordpress-body]";
4164 const STATUS_SEL = "[data-desktop-mode-my-wordpress-status]";
4165 function wpdConfirmGlobal(options) {
4166 const fn = window.wp?.desktop?.confirm;
4167 if (typeof fn !== "function") {
4168 return Promise.resolve(false);
4169 }
4170 return fn(options);
4171 }
4172 function openIframeWindow(opts) {
4173 const manager = window.wp?.desktop?.windowManager;
4174 if (!manager || typeof manager.open !== "function") {
4175 return;
4176 }
4177 manager.open({
4178 id: opts.id,
4179 url: opts.url,
4180 title: opts.title,
4181 icon: opts.icon
4182 });
4183 }
4184 function getThumbnail(item) {
4185 const media = item._embedded?.["wp:featuredmedia"]?.[0];
4186 if (!media) {
4187 return "";
4188 }
4189 const sizes = media.media_details?.sizes;
4190 const preferred = sizes?.medium?.source_url ?? sizes?.thumbnail?.source_url ?? sizes?.large?.source_url ?? media.source_url;
4191 return preferred ?? "";
4192 }
4193 function paintStatus(state, baseSegments, ctx) {
4194 const filtered = applyFilters(
4195 "desktop-mode.my-wordpress.status-bar",
4196 baseSegments,
4197 ctx
4198 );
4199 renderStatusBarSegments(
4200 state.statusBar,
4201 Array.isArray(filtered) ? filtered : baseSegments
4202 );
4203 }
4204 function pluralLabel(n, singular, plural) {
4205 return `${n.toLocaleString()} ${n === 1 ? singular : plural}`;
4206 }
4207 function navigate(state, route, opts = {}) {
4208 const sameRoute = routesEqual(state.route, route);
4209 if (!opts.fromBack && !sameRoute) {
4210 state.history.push(state.route);
4211 }
4212 clearTeardown(state);
4213 state.route = route;
4214 updateBreadcrumbs(state);
4215 state.body.replaceChildren();
4216 if (route.kind === "root") {
4217 renderRoot(state);
4218 return;
4219 }
4220 const entity = getEntity(route.entityId);
4221 if (!entity) {
4222 renderError(
4223 state,
4224 __("Unknown entity type.", "desktop-mode")
4225 );
4226 return;
4227 }
4228 if (route.kind === "list") {
4229 const renderer = getEntityRenderer(entity.kind);
4230 if (renderer) {
4231 const host = makeRenderHost(state);
4232 renderer(host, entity);
4233 return;
4234 }
4235 renderEntityList(state, entity);
4236 return;
4237 }
4238 if (route.kind === "detail") {
4239 renderDetail(state, entity, route.postId, route.postTitle);
4240 return;
4241 }
4242 if (route.kind === "sub-list") {
4243 renderSubList(
4244 state,
4245 entity,
4246 route.postId,
4247 route.postTitle,
4248 route.relation
4249 );
4250 return;
4251 }
4252 if (route.kind === "user-footprint") {
4253 renderUserFootprint(state, entity, route.userId, route.userName);
4254 return;
4255 }
4256 if (route.kind === "media-detail") {
4257 void renderMediaDetail(makeRenderHost(state), route.mediaId);
4258 return;
4259 }
4260 }
4261 function makeRenderHost(state) {
4262 return {
4263 body: state.body,
4264 route: state.route,
4265 navigate: (route) => navigate(state, route),
4266 addTeardown: (fn) => state.teardown.push(fn)
4267 };
4268 }
4269 function routesEqual(a, b) {
4270 if (a.kind !== b.kind) {
4271 return false;
4272 }
4273 switch (a.kind) {
4274 case "root":
4275 return true;
4276 case "list":
4277 return a.entityId === b.entityId;
4278 case "detail": {
4279 const o = b;
4280 return a.entityId === o.entityId && a.postId === o.postId;
4281 }
4282 case "sub-list": {
4283 const o = b;
4284 return a.entityId === o.entityId && a.postId === o.postId && a.relation === o.relation;
4285 }
4286 case "user-footprint": {
4287 const o = b;
4288 return a.entityId === o.entityId && a.userId === o.userId;
4289 }
4290 case "media-detail": {
4291 const o = b;
4292 return a.entityId === o.entityId && a.mediaId === o.mediaId;
4293 }
4294 default:
4295 return false;
4296 }
4297 }
4298 function parentRoute(route) {
4299 switch (route.kind) {
4300 case "root":
4301 return route;
4302 case "list":
4303 return { kind: "root" };
4304 case "detail":
4305 return { kind: "list", entityId: route.entityId };
4306 case "sub-list":
4307 return {
4308 kind: "detail",
4309 entityId: route.entityId,
4310 postId: route.postId,
4311 postTitle: route.postTitle
4312 };
4313 case "user-footprint":
4314 return { kind: "list", entityId: route.entityId };
4315 case "media-detail":
4316 return { kind: "list", entityId: route.entityId };
4317 default:
4318 return { kind: "root" };
4319 }
4320 }
4321 function clearTeardown(state) {
4322 for (const fn of state.teardown) {
4323 try {
4324 fn();
4325 } catch {
4326 }
4327 }
4328 state.teardown = [];
4329 }
4330 function updateBreadcrumbs(state) {
4331 const { route } = state;
4332 const segments = [];
4333 const isRoot = route.kind === "root";
4334 segments.push(
4335 isRoot ? { label: __("My WordPress", "desktop-mode") } : {
4336 label: __("My WordPress", "desktop-mode"),
4337 onClick: () => navigate(state, { kind: "root" })
4338 }
4339 );
4340 if (route.kind !== "root") {
4341 const entity = getEntity(route.entityId);
4342 const label = entity ? entity.label : route.entityId;
4343 segments.push(
4344 route.kind === "list" ? { label } : {
4345 label,
4346 onClick: () => navigate(state, {
4347 kind: "list",
4348 entityId: route.entityId
4349 })
4350 }
4351 );
4352 }
4353 if (route.kind === "detail" || route.kind === "sub-list") {
4354 const postTitle = route.postTitle;
4355 const entityId = route.entityId;
4356 const postId = route.postId;
4357 segments.push(
4358 route.kind === "detail" ? { label: postTitle } : {
4359 label: postTitle,
4360 onClick: () => navigate(state, {
4361 kind: "detail",
4362 entityId,
4363 postId,
4364 postTitle
4365 })
4366 }
4367 );
4368 }
4369 if (route.kind === "sub-list") {
4370 segments.push({ label: subRelationLabel(route.relation) });
4371 }
4372 if (route.kind === "user-footprint") {
4373 segments.push({
4374 label: sprintf(
4375 // translators: %s is a user display name.
4376 __("%s — activity footprint", "desktop-mode"),
4377 route.userName
4378 )
4379 });
4380 }
4381 if (route.kind === "media-detail") {
4382 segments.push({ label: route.mediaTitle });
4383 }
4384 renderBreadcrumbs(state.breadcrumbs, segments, {
4385 onBack: () => {
4386 const previous = state.history.pop();
4387 if (previous) {
4388 navigate(state, previous, { fromBack: true });
4389 return;
4390 }
4391 navigate(state, parentRoute(state.route), { fromBack: true });
4392 },
4393 backDisabled: isRoot && state.history.length === 0
4394 });
4395 }
4396 function subRelationLabel(relation) {
4397 switch (relation) {
4398 case "author":
4399 return __("Author", "desktop-mode");
4400 case "contributors":
4401 return __("Contributors", "desktop-mode");
4402 case "comments":
4403 return __("Comments", "desktop-mode");
4404 case "categories":
4405 return __("Categories", "desktop-mode");
4406 case "tags":
4407 return __("Tags", "desktop-mode");
4408 case "media":
4409 return __("Attached media", "desktop-mode");
4410 case "revisions":
4411 return __("Revisions", "desktop-mode");
4412 default:
4413 return relation;
4414 }
4415 }
4416 function renderRoot(state) {
4417 const cfg = getConfig();
4418 const grid = document.createElement("div");
4419 grid.className = "desktop-mode-my-wordpress__grid desktop-mode-my-wordpress__canvas";
4420 grid.setAttribute("role", "list");
4421 const layout = createTileLayout(grid, "root");
4422 const select = createTileSelector();
4423 const tilesByEntity = /* @__PURE__ */ new Map();
4424 cfg.entities.forEach((entity, idx) => {
4425 const tile = buildIconTile({
4426 role: "folder",
4427 icon: entity.icon,
4428 label: entity.label
4429 });
4430 tile.dataset.entityId = entity.id;
4431 tilesByEntity.set(entity.id, tile);
4432 const tileKey = `entity:${entity.id}`;
4433 const synthDate = new Date(2020, 0, 1 + idx).toISOString();
4434 layout.place(tile, tileKey, {
4435 name: entity.label,
4436 date: synthDate
4437 });
4438 tile.addEventListener("click", () => select(tile));
4439 tile.addEventListener("dblclick", (e) => {
4440 e.preventDefault();
4441 navigate(state, { kind: "list", entityId: entity.id });
4442 });
4443 grid.appendChild(tile);
4444 });
4445 cfg.entities.forEach((entity) => {
4446 void fetchEntityTotal(entity).then((total) => {
4447 if (state.route.kind !== "root") {
4448 return;
4449 }
4450 const tile = tilesByEntity.get(entity.id);
4451 if (!tile) {
4452 return;
4453 }
4454 const label = tile.querySelector(
4455 ".desktop-mode-file-tile__label"
4456 );
4457 if (label) {
4458 label.textContent = `${entity.label} · ${total.toLocaleString()}`;
4459 }
4460 }).catch(() => {
4461 });
4462 });
4463 state.body.appendChild(grid);
4464 const menu = attachIconCanvasMenu(grid, {
4465 scope: "my-wordpress:root",
4466 onSort: (mode) => layout.sort(mode)
4467 });
4468 state.teardown.push(() => menu.dispose());
4469 state.teardown.push(() => layout.dispose());
4470 paintStatus(
4471 state,
4472 [
4473 {
4474 id: "count",
4475 label: pluralLabel(cfg.entities.length, "folder", "folders"),
4476 align: "start",
4477 sort: 10
4478 }
4479 ],
4480 { view: "root" }
4481 );
4482 }
4483 function buildIconTile(spec) {
4484 return buildTileFromSpec({
4485 type: spec.role === "folder" ? "folder" : "__my-wordpress-entry",
4486 ref: spec.label,
4487 label: spec.label,
4488 icon: sanitizeClass(spec.icon),
4489 role: spec.role,
4490 extraClasses: [
4491 "desktop-mode-my-wordpress__tile",
4492 spec.role === "folder" ? "desktop-mode-my-wordpress__tile--folder" : "desktop-mode-my-wordpress__tile--entry"
4493 ]
4494 });
4495 }
4496 function renderError(state, message) {
4497 const empty = document.createElement("div");
4498 empty.className = "desktop-mode-my-wordpress__empty";
4499 empty.textContent = message;
4500 state.body.appendChild(empty);
4501 }
4502 const lastQueryByEntity = /* @__PURE__ */ new Map();
4503 function renderEntityList(state, entity) {
4504 const cfg = getConfig();
4505 const initialQuery = lastQueryByEntity.get(entity.id) ?? "";
4506 const toolbar = renderListToolbar({
4507 placeholder: sprintf(
4508 // translators: %s is a lowercased entity-type label (e.g. "posts", "pages").
4509 __("Search %s…", "desktop-mode"),
4510 entity.label.toLowerCase()
4511 ),
4512 ariaLabel: sprintf(
4513 // translators: %s is an entity-type label (e.g. "Posts", "Pages").
4514 __("Search %s", "desktop-mode"),
4515 entity.label
4516 ),
4517 initialValue: initialQuery,
4518 onSearchChange: (q) => {
4519 lastQueryByEntity.set(entity.id, q);
4520 void resetForSearch(q);
4521 }
4522 });
4523 state.body.appendChild(toolbar.host);
4524 state.teardown.push(() => toolbar.destroy());
4525 const split = document.createElement("div");
4526 split.className = "desktop-mode-my-wordpress__split";
4527 const left = document.createElement("div");
4528 left.className = "desktop-mode-my-wordpress__list";
4529 const tiles = document.createElement("div");
4530 tiles.className = "desktop-mode-my-wordpress__tiles desktop-mode-my-wordpress__canvas";
4531 tiles.setAttribute("role", "list");
4532 left.appendChild(tiles);
4533 const sentinel = document.createElement("div");
4534 sentinel.className = "desktop-mode-my-wordpress__sentinel";
4535 sentinel.setAttribute("aria-hidden", "true");
4536 left.appendChild(sentinel);
4537 const right = document.createElement("div");
4538 right.className = "desktop-mode-my-wordpress__preview";
4539 const previewEmpty = document.createElement("div");
4540 previewEmpty.className = "desktop-mode-my-wordpress__preview-empty";
4541 previewEmpty.textContent = __(
4542 "Select an entry to preview it here.",
4543 "desktop-mode"
4544 );
4545 right.appendChild(previewEmpty);
4546 split.appendChild(left);
4547 split.appendChild(right);
4548 state.body.appendChild(split);
4549 const tileLayout = createTileLayout(tiles, `entity:${entity.id}`);
4550 const menu = attachIconCanvasMenu(tiles, {
4551 scope: `my-wordpress:${entity.id}`,
4552 onSort: (mode) => tileLayout.sort(mode)
4553 });
4554 state.teardown.push(() => menu.dispose());
4555 const ctx = {
4556 page: 0,
4557 totalPages: 1,
4558 total: 0,
4559 loaded: 0,
4560 loading: false,
4561 done: false,
4562 tiles,
4563 sentinel,
4564 preview: right,
4565 selectedId: null,
4566 selectedTile: null,
4567 observer: null,
4568 layout: tileLayout,
4569 query: initialQuery,
4570 abort: null
4571 };
4572 state.teardown.push(() => tileLayout.dispose());
4573 state.teardown.push(() => ctx.abort?.abort());
4574 const repaintListStatus = () => {
4575 let itemLabel;
4576 if (ctx.total === 0 && ctx.loaded === 0) {
4577 itemLabel = pluralLabel(0, "item", "items");
4578 } else if (ctx.total > ctx.loaded && ctx.loaded > 0) {
4579 itemLabel = sprintf(
4580 // translators: 1: visible item count, 2: total item count.
4581 __("%1$d of %2$d items", "desktop-mode"),
4582 ctx.loaded,
4583 ctx.total
4584 );
4585 } else {
4586 itemLabel = pluralLabel(
4587 Math.max(ctx.total, ctx.loaded),
4588 "item",
4589 "items"
4590 );
4591 }
4592 const segments = [
4593 { id: "count", label: itemLabel, align: "start", sort: 10 }
4594 ];
4595 if (ctx.totalPages > 1) {
4596 segments.push({
4597 id: "page",
4598 label: sprintf(
4599 // translators: 1: current page, 2: total pages.
4600 __("Page %1$d of %2$d", "desktop-mode"),
4601 Math.max(ctx.page, 1),
4602 ctx.totalPages
4603 ),
4604 align: "end",
4605 sort: 10
4606 });
4607 }
4608 paintStatus(state, segments, {
4609 view: "list",
4610 entityId: entity.id
4611 });
4612 };
4613 repaintListStatus();
4614 const sentinelIsVisible = () => {
4615 const sr = sentinel.getBoundingClientRect();
4616 const rr = left.getBoundingClientRect();
4617 const slack = 200;
4618 return sr.top < rr.bottom + slack && sr.bottom > rr.top - slack;
4619 };
4620 const loadMore = async () => {
4621 if (ctx.loading || ctx.done) {
4622 return;
4623 }
4624 ctx.loading = true;
4625 const nextPage = ctx.page + 1;
4626 const isFirst = nextPage === 1;
4627 const queryAtFetchTime = ctx.query;
4628 showLoadingSkeleton(tiles, ctx.layout, isFirst);
4629 const controller = new AbortController();
4630 ctx.abort = controller;
4631 try {
4632 const result = await fetchEntityList(entity, {
4633 page: nextPage,
4634 perPage: cfg.perPage,
4635 search: queryAtFetchTime || void 0,
4636 signal: controller.signal
4637 });
4638 if (ctx.query !== queryAtFetchTime) {
4639 return;
4640 }
4641 ctx.page = nextPage;
4642 ctx.totalPages = result.totalPages;
4643 ctx.total = result.total;
4644 hideLoadingSkeleton(tiles);
4645 if (result.items.length === 0 && isFirst) {
4646 renderListEmpty(tiles, entity, queryAtFetchTime);
4647 ctx.done = true;
4648 repaintListStatus();
4649 return;
4650 }
4651 for (const item of result.items) {
4652 tiles.appendChild(buildEntityTile(state, ctx, entity, item));
4653 ctx.loaded += 1;
4654 }
4655 if (ctx.page >= ctx.totalPages) {
4656 ctx.done = true;
4657 }
4658 repaintListStatus();
4659 } catch (err) {
4660 if (isAbortError(err)) {
4661 return;
4662 }
4663 hideLoadingSkeleton(tiles);
4664 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
4665 renderListError(tiles, msg);
4666 ctx.done = true;
4667 } finally {
4668 ctx.loading = false;
4669 if (ctx.abort === controller) {
4670 ctx.abort = null;
4671 }
4672 }
4673 if (!ctx.done) {
4674 requestAnimationFrame(() => {
4675 if (sentinelIsVisible()) {
4676 void loadMore();
4677 }
4678 });
4679 }
4680 };
4681 const resetForSearch = async (q) => {
4682 ctx.abort?.abort();
4683 ctx.abort = null;
4684 ctx.query = q;
4685 tiles.classList.add(
4686 "desktop-mode-my-wordpress__tiles--searching"
4687 );
4688 hideLoadingSkeleton(tiles);
4689 const controller = new AbortController();
4690 ctx.abort = controller;
4691 ctx.loading = true;
4692 try {
4693 const result = await fetchEntityList(entity, {
4694 page: 1,
4695 perPage: cfg.perPage,
4696 search: q || void 0,
4697 signal: controller.signal
4698 });
4699 if (ctx.query !== q) {
4700 return;
4701 }
4702 tiles.replaceChildren();
4703 ctx.layout.clear();
4704 tiles.classList.remove(
4705 "desktop-mode-my-wordpress__tiles--searching"
4706 );
4707 ctx.page = 1;
4708 ctx.totalPages = result.totalPages;
4709 ctx.total = result.total;
4710 ctx.loaded = 0;
4711 ctx.done = ctx.page >= ctx.totalPages;
4712 ctx.selectedId = null;
4713 ctx.selectedTile = null;
4714 ctx.preview.replaceChildren();
4715 const emptyPreview = document.createElement("div");
4716 emptyPreview.className = "desktop-mode-my-wordpress__preview-empty";
4717 emptyPreview.textContent = __(
4718 "Select an entry to preview it here.",
4719 "desktop-mode"
4720 );
4721 ctx.preview.appendChild(emptyPreview);
4722 if (result.items.length === 0) {
4723 renderListEmpty(tiles, entity, q);
4724 ctx.done = true;
4725 } else {
4726 for (const item of result.items) {
4727 tiles.appendChild(
4728 buildEntityTile(state, ctx, entity, item)
4729 );
4730 ctx.loaded += 1;
4731 }
4732 }
4733 repaintListStatus();
4734 } catch (err) {
4735 if (isAbortError(err)) {
4736 return;
4737 }
4738 tiles.classList.remove(
4739 "desktop-mode-my-wordpress__tiles--searching"
4740 );
4741 tiles.replaceChildren();
4742 ctx.layout.clear();
4743 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
4744 renderListError(tiles, msg);
4745 ctx.done = true;
4746 } finally {
4747 ctx.loading = false;
4748 if (ctx.abort === controller) {
4749 ctx.abort = null;
4750 }
4751 }
4752 if (!ctx.done) {
4753 requestAnimationFrame(() => {
4754 if (sentinelIsVisible()) {
4755 void loadMore();
4756 }
4757 });
4758 }
4759 };
4760 if (typeof IntersectionObserver !== "undefined") {
4761 ctx.observer = new IntersectionObserver(
4762 (entries) => {
4763 for (const e of entries) {
4764 if (e.isIntersecting) {
4765 void loadMore();
4766 }
4767 }
4768 },
4769 { root: left, rootMargin: "200px 0px" }
4770 );
4771 ctx.observer.observe(sentinel);
4772 state.teardown.push(() => ctx.observer?.disconnect());
4773 }
4774 void loadMore();
4775 }
4776 function isAbortError(err) {
4777 return err instanceof DOMException && err.name === "AbortError";
4778 }
4779 function renderListEmpty(host, entity, query) {
4780 const empty = document.createElement("div");
4781 empty.className = "desktop-mode-my-wordpress__empty";
4782 if (query) {
4783 empty.textContent = sprintf(
4784 // translators: 1: search query, 2: lowercased entity-type label.
4785 __('No %2$s match "%1$s".', "desktop-mode"),
4786 query,
4787 entity.label.toLowerCase()
4788 );
4789 } else {
4790 empty.textContent = sprintf(
4791 // translators: %s is an entity-type label (e.g. "Posts", "Pages").
4792 __("No %s yet.", "desktop-mode"),
4793 entity.label.toLowerCase()
4794 );
4795 }
4796 host.appendChild(empty);
4797 }
4798 function renderListError(host, message) {
4799 const err = document.createElement("div");
4800 err.className = "desktop-mode-my-wordpress__error";
4801 err.textContent = message;
4802 host.appendChild(err);
4803 }
4804 function buildSkeletonTile(variant) {
4805 const tile = document.createElement("div");
4806 tile.className = "desktop-mode-my-wordpress__skeleton-tile";
4807 tile.dataset.loadingSkeleton = variant;
4808 tile.setAttribute("aria-hidden", "true");
4809 const icon = document.createElement("div");
4810 icon.className = "desktop-mode-my-wordpress__skeleton-icon";
4811 tile.appendChild(icon);
4812 const label = document.createElement("div");
4813 label.className = "desktop-mode-my-wordpress__skeleton-label";
4814 tile.appendChild(label);
4815 return tile;
4816 }
4817 const SKELETON_LABEL_WIDTHS = [72, 60, 82, 48, 70];
4818 const SKELETON_DELAY_STEPS = [0, 0.18, 0.36, 0.54, 0.12];
4819 function showLoadingSkeleton(host, layout, isFirst) {
4820 const variant = isFirst ? "first" : "more";
4821 if (host.querySelector(`[data-loading-skeleton="${variant}"]`)) {
4822 return;
4823 }
4824 const count = isFirst ? 8 : 4;
4825 const cells = layout.peekNextCells(count);
4826 let maxBottom = parseFloat(host.style.minHeight || "0");
4827 cells.forEach((cell, i) => {
4828 const tile = buildSkeletonTile(variant);
4829 tile.style.left = `${cell.x}px`;
4830 tile.style.top = `${cell.y}px`;
4831 tile.style.setProperty(
4832 "--desktop-mode-skeleton-delay",
4833 `${SKELETON_DELAY_STEPS[i % SKELETON_DELAY_STEPS.length]}s`
4834 );
4835 const label = tile.querySelector(
4836 ".desktop-mode-my-wordpress__skeleton-label"
4837 );
4838 if (label) {
4839 label.style.width = `${SKELETON_LABEL_WIDTHS[i % SKELETON_LABEL_WIDTHS.length]}%`;
4840 }
4841 host.appendChild(tile);
4842 maxBottom = Math.max(maxBottom, cell.y + TILE_H);
4843 });
4844 host.style.minHeight = `${maxBottom + TILE_PAD}px`;
4845 }
4846 function hideLoadingSkeleton(host) {
4847 host.querySelectorAll("[data-loading-skeleton]").forEach(
4848 (n) => n.remove()
4849 );
4850 }
4851 function buildEntityTile(state, ctx, entity, item) {
4852 const titleText = stripTags(item.title.rendered) || __("(no title)", "desktop-mode");
4853 const tile = buildIconTile({
4854 role: "entry",
4855 icon: entity.icon,
4856 label: titleText
4857 });
4858 tile.dataset.entryId = String(item.id);
4859 if (item.status) {
4860 tile.setAttribute("status", item.status);
4861 }
4862 attachTileDragOut(
4863 tile,
4864 {
4865 kind: "post",
4866 ref: String(item.id),
4867 title: titleText,
4868 icon: entity.icon,
4869 // Cross-frame bridge payload — the Gutenberg drop-receiver
4870 // turns this into a `core/paragraph` with an `<a href>` to
4871 // the permalink. Tiles without a `link` (very old REST
4872 // shapes / private posts) still drag-out for placement
4873 // purposes; the receiver no-ops on an empty url.
4874 bridgePayload: {
4875 kind: "post",
4876 id: item.id,
4877 postType: entity.id,
4878 url: item.link ?? "",
4879 title: titleText
4880 }
4881 },
4882 () => hideTooltip()
4883 );
4884 const lock = item.desktop_mode_lock ?? null;
4885 if (lock) {
4886 tile.classList.add("desktop-mode-my-wordpress__tile--locked");
4887 const badge = document.createElement("span");
4888 badge.className = "desktop-mode-my-wordpress__tile-lock dashicons dashicons-lock";
4889 badge.setAttribute("aria-hidden", "true");
4890 tile.appendChild(badge);
4891 const lockedAriaLabel = __(
4892 "%1$s — currently being edited by %2$s",
4893 "desktop-mode"
4894 );
4895 tile.setAttribute(
4896 "aria-label",
4897 sprintf(lockedAriaLabel, titleText, lock.userName)
4898 );
4899 }
4900 let tooltip = null;
4901 const showTooltip = (ev) => {
4902 if (!tooltip) {
4903 tooltip = buildTooltip(titleText, item);
4904 }
4905 document.body.appendChild(tooltip);
4906 positionTooltip(tooltip, ev);
4907 };
4908 const moveTooltip = (ev) => {
4909 if (tooltip && tooltip.isConnected) {
4910 positionTooltip(tooltip, ev);
4911 }
4912 };
4913 const hideTooltip = () => {
4914 if (tooltip && tooltip.isConnected) {
4915 tooltip.remove();
4916 }
4917 };
4918 tile.addEventListener("mouseenter", showTooltip);
4919 tile.addEventListener("mousemove", moveTooltip);
4920 tile.addEventListener("mouseleave", hideTooltip);
4921 state.teardown.push(hideTooltip);
4922 const tileKey = `entry:${item.id}`;
4923 ctx.layout.place(tile, tileKey, {
4924 name: titleText,
4925 date: item.date || (/* @__PURE__ */ new Date(0)).toISOString()
4926 });
4927 tile.addEventListener("click", () => {
4928 selectTile(state, ctx, tile, entity, item.id);
4929 });
4930 tile.addEventListener("dblclick", (e) => {
4931 e.preventDefault();
4932 hideTooltip();
4933 openEditor(entity, item.id, titleText);
4934 });
4935 tile.addEventListener("contextmenu", (e) => {
4936 e.preventDefault();
4937 hideTooltip();
4938 openTileMenu(state, ctx, entity, item, titleText, {
4939 x: e.clientX,
4940 y: e.clientY
4941 });
4942 });
4943 return tile;
4944 }
4945 function buildTooltip(title, item) {
4946 const tip = document.createElement("div");
4947 tip.className = "desktop-mode-my-wordpress__tooltip";
4948 tip.setAttribute("role", "tooltip");
4949 const heading = document.createElement("div");
4950 heading.className = "desktop-mode-my-wordpress__tooltip-title";
4951 heading.textContent = title;
4952 tip.appendChild(heading);
4953 const lock = item.desktop_mode_lock ?? null;
4954 if (lock) {
4955 const banner = document.createElement("div");
4956 banner.className = "desktop-mode-my-wordpress__tooltip-lock";
4957 const icon = document.createElement("span");
4958 icon.className = "dashicons dashicons-lock";
4959 icon.setAttribute("aria-hidden", "true");
4960 banner.appendChild(icon);
4961 const text = document.createElement("span");
4962 text.textContent = sprintf(
4963 // translators: %s is the user name currently editing the post.
4964 __("%s is currently editing", "desktop-mode"),
4965 lock.userName
4966 );
4967 banner.appendChild(text);
4968 tip.appendChild(banner);
4969 }
4970 const thumb = getThumbnail(item);
4971 if (thumb) {
4972 const img = document.createElement("img");
4973 img.className = "desktop-mode-my-wordpress__tooltip-thumb";
4974 img.src = thumb;
4975 img.alt = "";
4976 tip.appendChild(img);
4977 }
4978 const excerpt = stripTags(item.excerpt?.rendered ?? "");
4979 if (excerpt) {
4980 const p = document.createElement("p");
4981 p.className = "desktop-mode-my-wordpress__tooltip-excerpt";
4982 p.textContent = excerpt.length > 240 ? excerpt.slice(0, 237) + "" : excerpt;
4983 tip.appendChild(p);
4984 }
4985 return tip;
4986 }
4987 function positionTooltip(tip, ev) {
4988 const offset = 16;
4989 let x = ev.clientX + offset;
4990 let y = ev.clientY + offset;
4991 const rect = tip.getBoundingClientRect();
4992 if (x + rect.width > window.innerWidth - 8) {
4993 x = Math.max(8, ev.clientX - rect.width - offset);
4994 }
4995 if (y + rect.height > window.innerHeight - 8) {
4996 y = Math.max(8, ev.clientY - rect.height - offset);
4997 }
4998 tip.style.left = `${x}px`;
4999 tip.style.top = `${y}px`;
5000 }
5001 function selectTile(state, ctx, tile, entity, id) {
5002 if (ctx.selectedTile) {
5003 ctx.selectedTile.classList.remove(
5004 "desktop-mode-file-tile--selected"
5005 );
5006 }
5007 tile.classList.add("desktop-mode-file-tile--selected");
5008 ctx.selectedTile = tile;
5009 ctx.selectedId = id;
5010 void renderPreview(state, ctx, entity, id);
5011 }
5012 async function renderPreview(state, ctx, entity, id) {
5013 showPreviewLoading(ctx.preview);
5014 let detail;
5015 try {
5016 detail = await fetchEntityDetail(entity, id);
5017 } catch (err) {
5018 ctx.preview.replaceChildren();
5019 if (ctx.selectedId !== id) {
5020 return;
5021 }
5022 showPreviewError(ctx.preview, err);
5023 return;
5024 }
5025 if (ctx.selectedId !== id) {
5026 return;
5027 }
5028 appendPostArticle(ctx.preview, detail, entity, {
5029 onExplore: () => {
5030 navigate(state, {
5031 kind: "detail",
5032 entityId: entity.id,
5033 postId: detail.id,
5034 postTitle: stripTags(detail.title.rendered)
5035 });
5036 }
5037 });
5038 }
5039 function showPreviewLoading(host) {
5040 host.replaceChildren();
5041 const loading = document.createElement("div");
5042 loading.className = "desktop-mode-my-wordpress__preview-loading";
5043 const spinner = document.createElement("wpd-spinner");
5044 loading.appendChild(spinner);
5045 host.appendChild(loading);
5046 }
5047 function showPreviewError(host, err) {
5048 host.replaceChildren();
5049 const box = document.createElement("div");
5050 box.className = "desktop-mode-my-wordpress__error";
5051 box.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
5052 host.appendChild(box);
5053 }
5054 function appendPostArticle(host, detail, entity, opts = {}) {
5055 host.replaceChildren();
5056 const article = document.createElement("article");
5057 article.className = "desktop-mode-my-wordpress__article";
5058 const heading = document.createElement("h2");
5059 heading.className = "desktop-mode-my-wordpress__article-title";
5060 heading.textContent = stripTags(detail.title.rendered);
5061 article.appendChild(heading);
5062 const meta = buildPostMetaLine(detail);
5063 if (meta) {
5064 article.appendChild(meta);
5065 }
5066 const thumb = getThumbnail(detail);
5067 if (thumb) {
5068 const img = document.createElement("img");
5069 img.className = "desktop-mode-my-wordpress__article-hero";
5070 img.src = thumb;
5071 img.alt = "";
5072 article.appendChild(img);
5073 }
5074 const content = document.createElement("div");
5075 content.className = "desktop-mode-my-wordpress__article-content";
5076 content.innerHTML = detail.content.rendered;
5077 article.appendChild(content);
5078 const footer = document.createElement("footer");
5079 footer.className = "desktop-mode-my-wordpress__article-footer";
5080 if (opts.onExplore) {
5081 const exploreBtn = document.createElement("wpd-button");
5082 exploreBtn.setAttribute("variant", "secondary");
5083 exploreBtn.textContent = __("Explore details", "desktop-mode");
5084 exploreBtn.title = __(
5085 "See author, comments, categories, tags, attached media, and revisions for this entry.",
5086 "desktop-mode"
5087 );
5088 exploreBtn.addEventListener("click", () => {
5089 opts.onExplore?.();
5090 });
5091 footer.appendChild(exploreBtn);
5092 }
5093 const editBtn = document.createElement("wpd-button");
5094 editBtn.setAttribute("variant", "primary");
5095 editBtn.textContent = __("Open in editor", "desktop-mode");
5096 editBtn.addEventListener("click", () => {
5097 openEditor(entity, detail.id, stripTags(detail.title.rendered));
5098 });
5099 footer.appendChild(editBtn);
5100 article.appendChild(footer);
5101 host.appendChild(article);
5102 }
5103 function buildPostMetaLine(detail) {
5104 const parts = [];
5105 const author = detail._embedded?.author?.[0];
5106 if (author?.name) {
5107 parts.push(author.name);
5108 }
5109 if (detail.date) {
5110 try {
5111 parts.push(
5112 new Date(detail.date).toLocaleDateString(void 0, {
5113 year: "numeric",
5114 month: "long",
5115 day: "numeric"
5116 })
5117 );
5118 } catch {
5119 parts.push(detail.date);
5120 }
5121 }
5122 if (detail.status && detail.status !== "publish") {
5123 parts.push(detail.status);
5124 }
5125 if (parts.length === 0) {
5126 return null;
5127 }
5128 const line = document.createElement("p");
5129 line.className = "desktop-mode-my-wordpress__article-meta";
5130 line.textContent = parts.join(" · ");
5131 return line;
5132 }
5133 function renderDetail(state, entity, postId, postTitle) {
5134 const split = document.createElement("div");
5135 split.className = "desktop-mode-my-wordpress__split";
5136 const left = document.createElement("div");
5137 left.className = "desktop-mode-my-wordpress__list";
5138 const tiles = document.createElement("div");
5139 tiles.className = "desktop-mode-my-wordpress__tiles desktop-mode-my-wordpress__canvas";
5140 tiles.setAttribute("role", "list");
5141 left.appendChild(tiles);
5142 const right = document.createElement("div");
5143 right.className = "desktop-mode-my-wordpress__preview";
5144 showPreviewLoading(right);
5145 split.appendChild(left);
5146 split.appendChild(right);
5147 state.body.appendChild(split);
5148 const layout = createTileLayout(
5149 tiles,
5150 `detail:${entity.id}:${postId}`
5151 );
5152 const menu = attachIconCanvasMenu(tiles, {
5153 scope: `my-wordpress:${entity.id}:detail:${postId}`,
5154 onSort: (mode) => layout.sort(mode)
5155 });
5156 state.teardown.push(() => menu.dispose());
5157 state.teardown.push(() => layout.dispose());
5158 showLoadingSkeleton(tiles, layout, true);
5159 void (async () => {
5160 let detail;
5161 try {
5162 detail = await fetchEntityDetail(entity, postId);
5163 } catch (err) {
5164 hideLoadingSkeleton(tiles);
5165 renderListError(
5166 tiles,
5167 err instanceof Error ? err.message : __("Unknown error.", "desktop-mode")
5168 );
5169 showPreviewError(right, err);
5170 return;
5171 }
5172 if (state.route.kind !== "detail" || state.route.postId !== postId) {
5173 return;
5174 }
5175 hideLoadingSkeleton(tiles);
5176 const select = createTileSelector();
5177 const subFolders = [];
5178 let dateCounter = 0;
5179 const nextDate = () => new Date(2020, 0, 1 + dateCounter++).toISOString();
5180 const author = detail._embedded?.author?.[0];
5181 subFolders.push({
5182 relation: "author",
5183 label: author?.name ? sprintf(
5184 // translators: %s is an author display name.
5185 __("Author · %s", "desktop-mode"),
5186 author.name
5187 ) : __("Author", "desktop-mode"),
5188 icon: "dashicons-admin-users",
5189 count: 1,
5190 disabled: !detail.author,
5191 synthDate: nextDate()
5192 });
5193 const contributors = detail.desktop_mode_contributors ?? [];
5194 if (contributors.length > 0) {
5195 subFolders.push({
5196 relation: "contributors",
5197 label: sprintf(
5198 // translators: %d is a count of additional contributor users.
5199 _n(
5200 "Contributors · %d",
5201 "Contributors · %d",
5202 contributors.length
5203 ),
5204 contributors.length
5205 ),
5206 icon: "dashicons-groups",
5207 count: contributors.length,
5208 synthDate: nextDate()
5209 });
5210 }
5211 const commentsHref = (detail._links?.replies ?? [])[0];
5212 const commentCountFromLink = typeof commentsHref?.count === "number" ? commentsHref.count : null;
5213 const repliesEmbed = detail._embedded?.replies?.[0] ?? [];
5214 const commentCount = commentCountFromLink ?? repliesEmbed.length;
5215 subFolders.push({
5216 relation: "comments",
5217 label: sprintf(
5218 // translators: %d is a comment count.
5219 _n("Comments · %d", "Comments · %d", commentCount),
5220 commentCount
5221 ),
5222 icon: "dashicons-admin-comments",
5223 count: commentCount,
5224 disabled: detail.comment_status === "closed" && commentCount === 0,
5225 synthDate: nextDate()
5226 });
5227 const categoryIds = detail.categories ?? [];
5228 if (categoryIds.length > 0) {
5229 subFolders.push({
5230 relation: "categories",
5231 label: sprintf(
5232 // translators: %d is a category count.
5233 _n("Categories · %d", "Categories · %d", categoryIds.length),
5234 categoryIds.length
5235 ),
5236 icon: "dashicons-category",
5237 count: categoryIds.length,
5238 synthDate: nextDate()
5239 });
5240 }
5241 const tagIds = detail.tags ?? [];
5242 if (tagIds.length > 0) {
5243 subFolders.push({
5244 relation: "tags",
5245 label: sprintf(
5246 // translators: %d is a tag count.
5247 _n("Tags · %d", "Tags · %d", tagIds.length),
5248 tagIds.length
5249 ),
5250 icon: "dashicons-tag",
5251 count: tagIds.length,
5252 synthDate: nextDate()
5253 });
5254 }
5255 if (detail.featured_media && detail.featured_media > 0) {
5256 subFolders.push({
5257 relation: "media",
5258 label: __("Attached media", "desktop-mode"),
5259 icon: "dashicons-format-image",
5260 count: 1,
5261 synthDate: nextDate()
5262 });
5263 } else {
5264 subFolders.push({
5265 relation: "media",
5266 label: __("Attached media", "desktop-mode"),
5267 icon: "dashicons-admin-media",
5268 count: 0,
5269 synthDate: nextDate()
5270 });
5271 }
5272 subFolders.push({
5273 relation: "revisions",
5274 label: __("Revisions", "desktop-mode"),
5275 icon: "dashicons-backup",
5276 count: 0,
5277 synthDate: nextDate()
5278 });
5279 for (const sub of subFolders) {
5280 const tile = buildIconTile({
5281 role: "folder",
5282 icon: sub.icon,
5283 label: sub.label
5284 });
5285 tile.dataset.relation = sub.relation;
5286 if (sub.disabled) {
5287 tile.setAttribute("aria-disabled", "true");
5288 }
5289 const tileKey = `relation:${sub.relation}`;
5290 layout.place(tile, tileKey, {
5291 name: sub.label,
5292 date: sub.synthDate
5293 });
5294 tile.addEventListener("click", () => select(tile));
5295 if (!sub.disabled) {
5296 tile.addEventListener("dblclick", (e) => {
5297 e.preventDefault();
5298 navigate(state, {
5299 kind: "sub-list",
5300 entityId: entity.id,
5301 postId,
5302 postTitle,
5303 relation: sub.relation
5304 });
5305 });
5306 }
5307 tiles.appendChild(tile);
5308 }
5309 appendPostArticle(right, detail, entity);
5310 const segments = [
5311 {
5312 id: "count",
5313 label: pluralLabel(
5314 subFolders.length,
5315 "folder",
5316 "folders"
5317 ),
5318 align: "start",
5319 sort: 10
5320 }
5321 ];
5322 if (detail.status) {
5323 segments.push({
5324 id: "status",
5325 label: detail.status,
5326 align: "end",
5327 sort: 10
5328 });
5329 }
5330 paintStatus(state, segments, {
5331 view: "detail",
5332 entityId: entity.id,
5333 postId
5334 });
5335 })();
5336 paintStatus(
5337 state,
5338 [
5339 {
5340 id: "loading",
5341 label: __("Loading…", "desktop-mode"),
5342 align: "start",
5343 sort: 10
5344 }
5345 ],
5346 { view: "detail", entityId: entity.id, postId }
5347 );
5348 }
5349 function renderSubList(state, entity, postId, postTitle, relation) {
5350 const split = document.createElement("div");
5351 split.className = "desktop-mode-my-wordpress__split";
5352 const left = document.createElement("div");
5353 left.className = "desktop-mode-my-wordpress__list";
5354 const tiles = document.createElement("div");
5355 tiles.className = "desktop-mode-my-wordpress__tiles desktop-mode-my-wordpress__canvas";
5356 tiles.setAttribute("role", "list");
5357 left.appendChild(tiles);
5358 const right = document.createElement("div");
5359 right.className = "desktop-mode-my-wordpress__preview";
5360 const previewEmpty = document.createElement("div");
5361 previewEmpty.className = "desktop-mode-my-wordpress__preview-empty";
5362 previewEmpty.textContent = __(
5363 "Select an item to preview it here.",
5364 "desktop-mode"
5365 );
5366 right.appendChild(previewEmpty);
5367 split.appendChild(left);
5368 split.appendChild(right);
5369 state.body.appendChild(split);
5370 const layout = createTileLayout(
5371 tiles,
5372 `sub-list:${entity.id}:${postId}:${relation}`
5373 );
5374 const menu = attachIconCanvasMenu(tiles, {
5375 scope: `my-wordpress:${entity.id}:${relation}:${postId}`,
5376 onSort: (mode) => layout.sort(mode)
5377 });
5378 state.teardown.push(() => menu.dispose());
5379 state.teardown.push(() => layout.dispose());
5380 showLoadingSkeleton(tiles, layout, true);
5381 paintStatus(
5382 state,
5383 [
5384 {
5385 id: "loading",
5386 label: __("Loading…", "desktop-mode"),
5387 align: "start",
5388 sort: 10
5389 }
5390 ],
5391 { view: "sub-list", entityId: entity.id, postId, relation }
5392 );
5393 void (async () => {
5394 let items;
5395 try {
5396 items = await loadSubItems(entity, postId, relation);
5397 } catch (err) {
5398 hideLoadingSkeleton(tiles);
5399 renderListError(
5400 tiles,
5401 err instanceof Error ? err.message : __("Unknown error.", "desktop-mode")
5402 );
5403 return;
5404 }
5405 if (state.route.kind !== "sub-list" || state.route.postId !== postId || state.route.relation !== relation) {
5406 return;
5407 }
5408 hideLoadingSkeleton(tiles);
5409 paintStatus(
5410 state,
5411 [
5412 {
5413 id: "count",
5414 label: pluralLabel(items.length, "item", "items"),
5415 align: "start",
5416 sort: 10
5417 }
5418 ],
5419 {
5420 view: "sub-list",
5421 entityId: entity.id,
5422 postId,
5423 relation
5424 }
5425 );
5426 if (items.length === 0) {
5427 renderListEmptyMessage(
5428 tiles,
5429 emptySubListMessage(relation)
5430 );
5431 return;
5432 }
5433 let selectedKey = null;
5434 let selectedTile = null;
5435 for (const item of items) {
5436 const tile = buildIconTile({
5437 role: "entry",
5438 icon: item.icon,
5439 label: item.label
5440 });
5441 tile.dataset.subItemId = item.id;
5442 const tileKey = `sub:${item.id}`;
5443 layout.place(tile, tileKey, {
5444 name: item.label,
5445 date: item.date
5446 });
5447 tile.addEventListener("click", () => {
5448 if (selectedTile) {
5449 selectedTile.classList.remove(
5450 "desktop-mode-file-tile--selected"
5451 );
5452 }
5453 tile.classList.add(
5454 "desktop-mode-file-tile--selected"
5455 );
5456 selectedTile = tile;
5457 selectedKey = tileKey;
5458 showPreviewLoading(right);
5459 Promise.resolve(item.preview()).then((node) => {
5460 if (selectedKey !== tileKey) {
5461 return;
5462 }
5463 right.replaceChildren(node);
5464 }).catch((err) => {
5465 if (selectedKey !== tileKey) {
5466 return;
5467 }
5468 showPreviewError(right, err);
5469 });
5470 });
5471 tiles.appendChild(tile);
5472 }
5473 })();
5474 }
5475 function renderListEmptyMessage(host, message) {
5476 const empty = document.createElement("div");
5477 empty.className = "desktop-mode-my-wordpress__empty";
5478 empty.textContent = message;
5479 host.appendChild(empty);
5480 }
5481 function emptySubListMessage(relation) {
5482 switch (relation) {
5483 case "comments":
5484 return __("No comments on this post yet.", "desktop-mode");
5485 case "categories":
5486 return __("No categories assigned.", "desktop-mode");
5487 case "tags":
5488 return __("No tags assigned.", "desktop-mode");
5489 case "media":
5490 return __("No media attached to this post.", "desktop-mode");
5491 case "revisions":
5492 return __("No revisions yet.", "desktop-mode");
5493 case "author":
5494 return __("No author available.", "desktop-mode");
5495 case "contributors":
5496 return __("No additional contributors.", "desktop-mode");
5497 default:
5498 return __("Nothing to show.", "desktop-mode");
5499 }
5500 }
5501 async function loadSubItems(entity, postId, relation) {
5502 if (relation === "comments") {
5503 const comments = await fetchComments(postId);
5504 return comments.map(commentToView);
5505 }
5506 if (relation === "media") {
5507 const detail = await fetchEntityDetail(entity, postId);
5508 const ids = /* @__PURE__ */ new Set();
5509 if (detail.featured_media && detail.featured_media > 0) {
5510 ids.add(detail.featured_media);
5511 }
5512 const serverList = detail.desktop_mode_attached_media;
5513 if (Array.isArray(serverList) && serverList.length > 0) {
5514 for (const id of serverList) {
5515 if (typeof id === "number" && id > 0) {
5516 ids.add(id);
5517 }
5518 }
5519 } else {
5520 extractContentMediaIds(detail.content?.rendered ?? "").forEach(
5521 (id) => ids.add(id)
5522 );
5523 }
5524 const [batched, parentAttached] = await Promise.all([
5525 fetchMediaByIds(Array.from(ids)).catch(() => []),
5526 fetchAttachedMedia(postId).catch(() => [])
5527 ]);
5528 const seen = /* @__PURE__ */ new Set();
5529 const merged = [];
5530 const featuredId = detail.featured_media ?? 0;
5531 const orderedFromBatch = batched.slice().sort((a, b) => {
5532 if (a.id === featuredId && b.id !== featuredId) {
5533 return -1;
5534 }
5535 if (b.id === featuredId && a.id !== featuredId) {
5536 return 1;
5537 }
5538 return 0;
5539 });
5540 for (const m of [...orderedFromBatch, ...parentAttached]) {
5541 if (seen.has(m.id)) {
5542 continue;
5543 }
5544 seen.add(m.id);
5545 merged.push(m);
5546 }
5547 return merged.map(mediaToView);
5548 }
5549 if (relation === "categories" || relation === "tags") {
5550 const detail = await fetchEntityDetail(entity, postId);
5551 const ids = relation === "categories" ? detail.categories ?? [] : detail.tags ?? [];
5552 const terms = await fetchTerms(
5553 relation === "categories" ? "categories" : "tags",
5554 ids
5555 );
5556 return terms.map(termToView);
5557 }
5558 if (relation === "author") {
5559 const detail = await fetchEntityDetail(entity, postId);
5560 if (!detail.author) {
5561 return [];
5562 }
5563 const user = await fetchUser(detail.author);
5564 return [userToView(user)];
5565 }
5566 if (relation === "contributors") {
5567 const detail = await fetchEntityDetail(entity, postId);
5568 const contribs = detail.desktop_mode_contributors ?? [];
5569 return contribs.map(contributorToView);
5570 }
5571 if (relation === "revisions") {
5572 const revs = await fetchRevisions(entity, postId);
5573 const ordered = revs.slice().sort((a, b) => {
5574 const ta = Date.parse(a.modified || a.date || "");
5575 const tb = Date.parse(b.modified || b.date || "");
5576 return tb - ta;
5577 });
5578 return ordered.map((r) => revisionToView(r, entity, postId));
5579 }
5580 return [];
5581 }
5582 function commentToView(c) {
5583 const author = c.author_name || __("Anonymous", "desktop-mode");
5584 return {
5585 id: `comment:${c.id}`,
5586 icon: "dashicons-admin-comments",
5587 label: author,
5588 date: c.date,
5589 preview: async () => renderCommentDossier(c)
5590 };
5591 }
5592 async function renderCommentDossier(c) {
5593 let stats = null;
5594 try {
5595 stats = await fetchCommentStats(c.id);
5596 } catch {
5597 stats = null;
5598 }
5599 const wrap = document.createElement("div");
5600 wrap.className = "desktop-mode-my-wordpress__article desktop-mode-my-wordpress__comment";
5601 if (!stats) {
5602 appendCommentHeader(wrap, {
5603 authorName: c.author_name || __("Anonymous", "desktop-mode"),
5604 avatarUrl: c.author_avatar_urls ? pickAvatar(c.author_avatar_urls) ?? "" : "",
5605 authorLink: "",
5606 authorWebsite: "",
5607 status: c.status || "approved",
5608 date: c.date,
5609 editLink: "",
5610 totalApproved: 0
5611 });
5612 const body2 = document.createElement("div");
5613 body2.className = "desktop-mode-my-wordpress__article-content desktop-mode-my-wordpress__comment-body";
5614 body2.innerHTML = c.content.rendered;
5615 wrap.appendChild(body2);
5616 return wrap;
5617 }
5618 const { author, comment, post, parent, replies } = stats;
5619 appendCommentHeader(wrap, {
5620 authorName: author.displayName || author.name || __("Anonymous", "desktop-mode"),
5621 avatarUrl: author.avatarUrl,
5622 authorLink: author.profileLink ?? "",
5623 authorWebsite: author.url ?? "",
5624 status: comment.status,
5625 date: comment.date,
5626 editLink: comment.editLink,
5627 totalApproved: author.totalApprovedComments
5628 });
5629 if (parent) {
5630 const quote = document.createElement("blockquote");
5631 quote.className = "desktop-mode-my-wordpress__comment-quote";
5632 const lead = document.createElement("div");
5633 lead.className = "desktop-mode-my-wordpress__comment-quote-lead";
5634 lead.textContent = sprintf(
5635 // translators: %s is the parent comment's author name.
5636 __("In reply to %s", "desktop-mode"),
5637 parent.authorName
5638 );
5639 quote.appendChild(lead);
5640 const excerpt = document.createElement("p");
5641 excerpt.textContent = parent.excerpt || "";
5642 quote.appendChild(excerpt);
5643 wrap.appendChild(quote);
5644 }
5645 const body = document.createElement("div");
5646 body.className = "desktop-mode-my-wordpress__article-content desktop-mode-my-wordpress__comment-body";
5647 body.innerHTML = comment.rendered;
5648 wrap.appendChild(body);
5649 if (post) {
5650 const section = document.createElement("section");
5651 section.className = "desktop-mode-my-wordpress__user-section";
5652 const h = document.createElement("h3");
5653 h.textContent = __("On post", "desktop-mode");
5654 section.appendChild(h);
5655 const card = document.createElement("div");
5656 card.className = "desktop-mode-my-wordpress__comment-post";
5657 const titleEl = document.createElement("a");
5658 titleEl.className = "desktop-mode-my-wordpress__comment-post-title";
5659 titleEl.href = post.link;
5660 titleEl.target = "_blank";
5661 titleEl.rel = "noopener noreferrer";
5662 titleEl.textContent = post.title || `#${post.id}`;
5663 card.appendChild(titleEl);
5664 const meta = document.createElement("div");
5665 meta.className = "desktop-mode-my-wordpress__comment-post-meta";
5666 const parts = [];
5667 parts.push(formatDate(post.date));
5668 if (post.author?.name) {
5669 parts.push(post.author.name);
5670 }
5671 if (post.status && post.status !== "publish") {
5672 parts.push(post.status);
5673 }
5674 meta.textContent = parts.join(" · ");
5675 card.appendChild(meta);
5676 section.appendChild(card);
5677 wrap.appendChild(section);
5678 }
5679 if (replies.length > 0) {
5680 const section = document.createElement("section");
5681 section.className = "desktop-mode-my-wordpress__user-section";
5682 const h = document.createElement("h3");
5683 h.textContent = sprintf(
5684 // translators: %d is the number of direct replies to a comment.
5685 _n("Reply (%d)", "Replies (%d)", replies.length),
5686 replies.length
5687 );
5688 section.appendChild(h);
5689 const list = document.createElement("ul");
5690 list.className = "desktop-mode-my-wordpress__comment-replies";
5691 for (const r of replies) {
5692 const li = document.createElement("li");
5693 li.className = "desktop-mode-my-wordpress__comment-reply";
5694 if (r.avatarUrl) {
5695 const img = document.createElement("img");
5696 img.src = r.avatarUrl;
5697 img.alt = "";
5698 img.className = "desktop-mode-my-wordpress__comment-reply-avatar";
5699 li.appendChild(img);
5700 }
5701 const txt = document.createElement("div");
5702 txt.className = "desktop-mode-my-wordpress__comment-reply-text";
5703 const head = document.createElement("div");
5704 head.className = "desktop-mode-my-wordpress__comment-reply-head";
5705 const who = document.createElement("span");
5706 who.className = "desktop-mode-my-wordpress__comment-reply-name";
5707 who.textContent = r.authorName || __("Anonymous", "desktop-mode");
5708 head.appendChild(who);
5709 const when = document.createElement("span");
5710 when.className = "desktop-mode-my-wordpress__comment-reply-when";
5711 when.textContent = formatDate(r.date);
5712 head.appendChild(when);
5713 txt.appendChild(head);
5714 const ex = document.createElement("p");
5715 ex.className = "desktop-mode-my-wordpress__comment-reply-excerpt";
5716 ex.textContent = r.excerpt || "";
5717 txt.appendChild(ex);
5718 li.appendChild(txt);
5719 list.appendChild(li);
5720 }
5721 section.appendChild(list);
5722 wrap.appendChild(section);
5723 }
5724 if (comment.ip || comment.userAgent) {
5725 const dl = document.createElement("dl");
5726 dl.className = "desktop-mode-my-wordpress__user-milestones";
5727 if (comment.ip) {
5728 const dt = document.createElement("dt");
5729 dt.textContent = __("IP", "desktop-mode");
5730 dl.appendChild(dt);
5731 const dd = document.createElement("dd");
5732 dd.textContent = comment.ip;
5733 dl.appendChild(dd);
5734 }
5735 if (comment.userAgent) {
5736 const dt = document.createElement("dt");
5737 dt.textContent = __("User agent", "desktop-mode");
5738 dl.appendChild(dt);
5739 const dd = document.createElement("dd");
5740 dd.textContent = comment.userAgent;
5741 dl.appendChild(dd);
5742 }
5743 wrap.appendChild(dl);
5744 }
5745 return wrap;
5746 }
5747 function appendCommentHeader(host, header) {
5748 const wrap = document.createElement("header");
5749 wrap.className = "desktop-mode-my-wordpress__user-header";
5750 if (header.avatarUrl) {
5751 const img = document.createElement("img");
5752 img.src = header.avatarUrl;
5753 img.alt = "";
5754 img.className = "desktop-mode-my-wordpress__user-avatar";
5755 wrap.appendChild(img);
5756 }
5757 const right = document.createElement("div");
5758 right.className = "desktop-mode-my-wordpress__user-headline";
5759 const h = document.createElement("h2");
5760 h.className = "desktop-mode-my-wordpress__article-title";
5761 h.textContent = header.authorName;
5762 right.appendChild(h);
5763 const badges = document.createElement("div");
5764 badges.className = "desktop-mode-my-wordpress__user-roles";
5765 const status = document.createElement("span");
5766 status.className = "desktop-mode-my-wordpress__user-role desktop-mode-my-wordpress__comment-status--" + (header.status || "approved");
5767 status.textContent = header.status || "approved";
5768 badges.appendChild(status);
5769 const dateBadge = document.createElement("span");
5770 dateBadge.className = "desktop-mode-my-wordpress__user-role desktop-mode-my-wordpress__comment-date-badge";
5771 dateBadge.textContent = formatDate(header.date);
5772 badges.appendChild(dateBadge);
5773 if (header.totalApproved > 1) {
5774 const totalBadge = document.createElement("span");
5775 totalBadge.className = "desktop-mode-my-wordpress__user-role";
5776 totalBadge.textContent = sprintf(
5777 // translators: %d is a comment count for a particular author.
5778 _n(
5779 "%d comment site-wide",
5780 "%d comments site-wide",
5781 header.totalApproved
5782 ),
5783 header.totalApproved
5784 );
5785 badges.appendChild(totalBadge);
5786 }
5787 right.appendChild(badges);
5788 const links = document.createElement("div");
5789 links.className = "desktop-mode-my-wordpress__user-links";
5790 if (header.authorLink) {
5791 const a = document.createElement("a");
5792 a.href = header.authorLink;
5793 a.target = "_blank";
5794 a.rel = "noopener noreferrer";
5795 a.textContent = __("Author archive", "desktop-mode");
5796 links.appendChild(a);
5797 }
5798 if (header.authorWebsite) {
5799 const a = document.createElement("a");
5800 a.href = header.authorWebsite;
5801 a.target = "_blank";
5802 a.rel = "noopener noreferrer";
5803 a.textContent = __("Website", "desktop-mode");
5804 links.appendChild(a);
5805 }
5806 if (header.editLink) {
5807 const a = document.createElement("a");
5808 a.href = header.editLink;
5809 a.target = "_blank";
5810 a.rel = "noopener noreferrer";
5811 a.textContent = __("Moderate", "desktop-mode");
5812 links.appendChild(a);
5813 }
5814 if (links.childElementCount > 0) {
5815 right.appendChild(links);
5816 }
5817 wrap.appendChild(right);
5818 host.appendChild(wrap);
5819 }
5820 function userToView(u) {
5821 return {
5822 id: `user:${u.id}`,
5823 icon: "dashicons-admin-users",
5824 label: u.name || u.slug || `#${u.id}`,
5825 date: (/* @__PURE__ */ new Date(0)).toISOString(),
5826 preview: async () => {
5827 const fallbackName = u.name || u.slug || `#${u.id}`;
5828 const fallbackAvatar = pickAvatar(u.avatar_urls) ?? "";
5829 return renderUserDossier({
5830 userId: u.id,
5831 fallbackName,
5832 fallbackAvatar,
5833 fallbackDescription: u.description ?? ""
5834 });
5835 }
5836 };
5837 }
5838 function contributorToView(c) {
5839 return {
5840 id: `contributor:${c.userId}`,
5841 icon: "dashicons-admin-users",
5842 label: c.userName || `#${c.userId}`,
5843 date: (/* @__PURE__ */ new Date(0)).toISOString(),
5844 preview: async () => renderUserDossier({
5845 userId: c.userId,
5846 fallbackName: c.userName,
5847 fallbackAvatar: c.userAvatarUrl,
5848 fallbackDescription: ""
5849 })
5850 };
5851 }
5852 async function renderUserDossier(opts) {
5853 let stats = null;
5854 try {
5855 stats = await fetchUserStats(opts.userId);
5856 } catch {
5857 stats = null;
5858 }
5859 const wrap = document.createElement("div");
5860 wrap.className = "desktop-mode-my-wordpress__article desktop-mode-my-wordpress__user";
5861 if (!stats) {
5862 let basic = null;
5863 try {
5864 basic = await fetchUser(opts.userId);
5865 } catch {
5866 basic = null;
5867 }
5868 appendUserHeader(wrap, {
5869 name: basic?.name ?? opts.fallbackName,
5870 avatarUrl: basic && pickAvatar(basic.avatar_urls) || opts.fallbackAvatar,
5871 roles: [],
5872 website: "",
5873 link: basic?.link ?? ""
5874 });
5875 const desc = basic?.description ?? opts.fallbackDescription;
5876 if (desc) {
5877 const bio = document.createElement("div");
5878 bio.className = "desktop-mode-my-wordpress__user-bio";
5879 bio.textContent = desc;
5880 wrap.appendChild(bio);
5881 }
5882 return wrap;
5883 }
5884 const { profile, counts, recent, topTerms, milestones, activity: activity2 } = stats;
5885 appendUserHeader(wrap, {
5886 name: profile.name || opts.fallbackName,
5887 avatarUrl: profile.avatarUrl || opts.fallbackAvatar,
5888 roles: profile.roleLabels ?? [],
5889 website: profile.website,
5890 link: profile.link
5891 });
5892 if (profile.description) {
5893 const bio = document.createElement("div");
5894 bio.className = "desktop-mode-my-wordpress__user-bio";
5895 bio.textContent = profile.description;
5896 wrap.appendChild(bio);
5897 }
5898 const cards = document.createElement("div");
5899 cards.className = "desktop-mode-my-wordpress__user-stats";
5900 cards.appendChild(
5901 buildStatCard(
5902 counts.posts.total.toLocaleString(),
5903 __("Posts", "desktop-mode"),
5904 counts.posts.publish > 0 ? sprintf(
5905 // translators: %d is a published-post count.
5906 __("%d published", "desktop-mode"),
5907 counts.posts.publish
5908 ) : ""
5909 )
5910 );
5911 cards.appendChild(
5912 buildStatCard(
5913 counts.pages.total.toLocaleString(),
5914 __("Pages", "desktop-mode"),
5915 counts.pages.publish > 0 ? sprintf(
5916 // translators: %d is a published-page count.
5917 __("%d published", "desktop-mode"),
5918 counts.pages.publish
5919 ) : ""
5920 )
5921 );
5922 cards.appendChild(
5923 buildStatCard(
5924 counts.commentsReceived.toLocaleString(),
5925 __("Comments received", "desktop-mode"),
5926 ""
5927 )
5928 );
5929 cards.appendChild(
5930 buildStatCard(
5931 counts.commentsLeft.toLocaleString(),
5932 __("Comments left", "desktop-mode"),
5933 ""
5934 )
5935 );
5936 wrap.appendChild(cards);
5937 const spark = buildActivitySparkline(activity2);
5938 if (spark) {
5939 wrap.appendChild(spark);
5940 }
5941 const milestoneRow = buildMilestonesRow(profile, milestones);
5942 if (milestoneRow) {
5943 wrap.appendChild(milestoneRow);
5944 }
5945 if (recent.length > 0) {
5946 const section = document.createElement("section");
5947 section.className = "desktop-mode-my-wordpress__user-section";
5948 const h = document.createElement("h3");
5949 h.textContent = __("Recent posts", "desktop-mode");
5950 section.appendChild(h);
5951 const ul = document.createElement("ul");
5952 ul.className = "desktop-mode-my-wordpress__user-recent";
5953 for (const r of recent) {
5954 const li = document.createElement("li");
5955 const a = document.createElement("a");
5956 a.href = r.link;
5957 a.target = "_blank";
5958 a.rel = "noopener noreferrer";
5959 a.textContent = r.title || `#${r.id}`;
5960 li.appendChild(a);
5961 const meta = document.createElement("span");
5962 meta.className = "desktop-mode-my-wordpress__user-recent-meta";
5963 meta.textContent = `${formatDate(r.date)} · ${r.status}`;
5964 li.appendChild(meta);
5965 ul.appendChild(li);
5966 }
5967 section.appendChild(ul);
5968 wrap.appendChild(section);
5969 }
5970 if (topTerms.length > 0) {
5971 const section = document.createElement("section");
5972 section.className = "desktop-mode-my-wordpress__user-section";
5973 const h = document.createElement("h3");
5974 h.textContent = __("Top categories & tags", "desktop-mode");
5975 section.appendChild(h);
5976 const chips = document.createElement("div");
5977 chips.className = "desktop-mode-my-wordpress__user-chips";
5978 for (const t of topTerms) {
5979 const chip = document.createElement("span");
5980 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");
5981 const name = document.createElement("span");
5982 name.textContent = t.name;
5983 chip.appendChild(name);
5984 const count = document.createElement("span");
5985 count.className = "desktop-mode-my-wordpress__user-chip-count";
5986 count.textContent = String(t.count);
5987 chip.appendChild(count);
5988 chips.appendChild(chip);
5989 }
5990 section.appendChild(chips);
5991 wrap.appendChild(section);
5992 }
5993 return wrap;
5994 }
5995 function appendUserHeader(host, header) {
5996 const wrap = document.createElement("header");
5997 wrap.className = "desktop-mode-my-wordpress__user-header";
5998 if (header.avatarUrl) {
5999 const img = document.createElement("img");
6000 img.src = header.avatarUrl;
6001 img.alt = "";
6002 img.className = "desktop-mode-my-wordpress__user-avatar";
6003 wrap.appendChild(img);
6004 }
6005 const right = document.createElement("div");
6006 right.className = "desktop-mode-my-wordpress__user-headline";
6007 const h = document.createElement("h2");
6008 h.className = "desktop-mode-my-wordpress__article-title";
6009 h.textContent = header.name;
6010 right.appendChild(h);
6011 if (header.roles.length > 0) {
6012 const rolesRow = document.createElement("div");
6013 rolesRow.className = "desktop-mode-my-wordpress__user-roles";
6014 for (const r of header.roles) {
6015 const badge = document.createElement("span");
6016 badge.className = "desktop-mode-my-wordpress__user-role";
6017 badge.textContent = r;
6018 rolesRow.appendChild(badge);
6019 }
6020 right.appendChild(rolesRow);
6021 }
6022 const links = document.createElement("div");
6023 links.className = "desktop-mode-my-wordpress__user-links";
6024 if (header.link) {
6025 const a = document.createElement("a");
6026 a.href = header.link;
6027 a.target = "_blank";
6028 a.rel = "noopener noreferrer";
6029 a.textContent = __("Author archive", "desktop-mode");
6030 links.appendChild(a);
6031 }
6032 if (header.website) {
6033 const a = document.createElement("a");
6034 a.href = header.website;
6035 a.target = "_blank";
6036 a.rel = "noopener noreferrer";
6037 a.textContent = __("Website", "desktop-mode");
6038 links.appendChild(a);
6039 }
6040 if (links.childElementCount > 0) {
6041 right.appendChild(links);
6042 }
6043 wrap.appendChild(right);
6044 host.appendChild(wrap);
6045 }
6046 function buildStatCard(value, label, caption) {
6047 const card = document.createElement("div");
6048 card.className = "desktop-mode-my-wordpress__user-stat";
6049 const v = document.createElement("span");
6050 v.className = "desktop-mode-my-wordpress__user-stat-value";
6051 v.textContent = value;
6052 card.appendChild(v);
6053 const l = document.createElement("span");
6054 l.className = "desktop-mode-my-wordpress__user-stat-label";
6055 l.textContent = label;
6056 card.appendChild(l);
6057 if (caption) {
6058 const c = document.createElement("span");
6059 c.className = "desktop-mode-my-wordpress__user-stat-caption";
6060 c.textContent = caption;
6061 card.appendChild(c);
6062 }
6063 return card;
6064 }
6065 function buildActivitySparkline(activity2) {
6066 if (activity2.length === 0) {
6067 return null;
6068 }
6069 const now = /* @__PURE__ */ new Date();
6070 const months = [];
6071 for (let i = 11; i >= 0; i -= 1) {
6072 const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
6073 const ym = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
6074 const found = activity2.find((a) => a.ym === ym);
6075 months.push({
6076 ym,
6077 count: found?.count ?? 0,
6078 label: d.toLocaleString(void 0, { month: "short" })
6079 });
6080 }
6081 const max = Math.max(1, ...months.map((m) => m.count));
6082 const wrap = document.createElement("section");
6083 wrap.className = "desktop-mode-my-wordpress__user-section desktop-mode-my-wordpress__user-spark";
6084 const h = document.createElement("h3");
6085 h.textContent = __("Activity (last 12 months)", "desktop-mode");
6086 wrap.appendChild(h);
6087 const chart = document.createElement("div");
6088 chart.className = "desktop-mode-my-wordpress__user-spark-chart";
6089 for (const m of months) {
6090 const col = document.createElement("div");
6091 col.className = "desktop-mode-my-wordpress__user-spark-col";
6092 const bar = document.createElement("div");
6093 bar.className = "desktop-mode-my-wordpress__user-spark-bar";
6094 bar.style.height = `${Math.round(m.count / max * 100)}%`;
6095 bar.title = sprintf(
6096 // translators: 1: month label, 2: post count.
6097 __("%1$s · %2$d posts", "desktop-mode"),
6098 m.label,
6099 m.count
6100 );
6101 if (m.count === 0) {
6102 bar.classList.add("desktop-mode-my-wordpress__user-spark-bar--empty");
6103 }
6104 col.appendChild(bar);
6105 const lbl = document.createElement("span");
6106 lbl.className = "desktop-mode-my-wordpress__user-spark-label";
6107 lbl.textContent = m.label;
6108 col.appendChild(lbl);
6109 chart.appendChild(col);
6110 }
6111 wrap.appendChild(chart);
6112 return wrap;
6113 }
6114 function buildMilestonesRow(profile, milestones) {
6115 const items = [];
6116 if (profile.registered) {
6117 items.push({
6118 label: __("Member since", "desktop-mode"),
6119 value: formatYearMonth(profile.registered)
6120 });
6121 }
6122 if (milestones.firstPublished) {
6123 items.push({
6124 label: __("First published", "desktop-mode"),
6125 value: formatYearMonth(milestones.firstPublished)
6126 });
6127 }
6128 if (milestones.lastPublished) {
6129 items.push({
6130 label: __("Last published", "desktop-mode"),
6131 value: formatYearMonth(milestones.lastPublished)
6132 });
6133 }
6134 if (items.length === 0) {
6135 return null;
6136 }
6137 const dl = document.createElement("dl");
6138 dl.className = "desktop-mode-my-wordpress__user-milestones";
6139 for (const item of items) {
6140 const dt = document.createElement("dt");
6141 dt.textContent = item.label;
6142 dl.appendChild(dt);
6143 const dd = document.createElement("dd");
6144 dd.textContent = item.value;
6145 dl.appendChild(dd);
6146 }
6147 return dl;
6148 }
6149 function formatYearMonth(iso) {
6150 if (!iso) {
6151 return "";
6152 }
6153 try {
6154 return new Date(iso).toLocaleString(void 0, {
6155 year: "numeric",
6156 month: "long"
6157 });
6158 } catch {
6159 return iso;
6160 }
6161 }
6162 function pickAvatar(avatars) {
6163 if (!avatars) {
6164 return null;
6165 }
6166 return avatars["96"] ?? avatars["48"] ?? avatars["24"] ?? Object.values(avatars)[0] ?? null;
6167 }
6168 function termToView(t) {
6169 return {
6170 id: `term:${t.id}`,
6171 icon: t.taxonomy === "post_tag" ? "dashicons-tag" : "dashicons-category",
6172 label: t.name,
6173 date: (/* @__PURE__ */ new Date(0)).toISOString(),
6174 preview: async () => renderTermDossier(t)
6175 };
6176 }
6177 async function renderTermDossier(t) {
6178 let stats = null;
6179 try {
6180 stats = await fetchTermStats(t.taxonomy, t.id);
6181 } catch {
6182 stats = null;
6183 }
6184 const wrap = document.createElement("div");
6185 wrap.className = "desktop-mode-my-wordpress__article desktop-mode-my-wordpress__term";
6186 if (!stats) {
6187 appendTermHeader(wrap, {
6188 name: t.name,
6189 taxonomyLabel: t.taxonomy,
6190 isTag: t.taxonomy === "post_tag",
6191 count: t.count ?? 0,
6192 link: t.link ?? "",
6193 parentName: ""
6194 });
6195 if (t.description) {
6196 const body = document.createElement("div");
6197 body.className = "desktop-mode-my-wordpress__user-bio";
6198 body.innerHTML = t.description;
6199 wrap.appendChild(body);
6200 }
6201 return wrap;
6202 }
6203 const { profile, counts, recent, topAuthors, coTerms, milestones, activity: activity2 } = stats;
6204 appendTermHeader(wrap, {
6205 name: profile.name,
6206 taxonomyLabel: profile.taxonomyLabel || profile.taxonomy,
6207 isTag: profile.taxonomy === "post_tag",
6208 count: profile.storedCount,
6209 link: profile.link,
6210 parentName: profile.parentName ?? ""
6211 });
6212 if (profile.description) {
6213 const bio = document.createElement("div");
6214 bio.className = "desktop-mode-my-wordpress__user-bio";
6215 bio.innerHTML = profile.description;
6216 wrap.appendChild(bio);
6217 }
6218 const cards = document.createElement("div");
6219 cards.className = "desktop-mode-my-wordpress__user-stats";
6220 cards.appendChild(
6221 buildStatCard(
6222 counts.posts.total.toLocaleString(),
6223 __("Posts", "desktop-mode"),
6224 counts.posts.publish > 0 ? sprintf(
6225 // translators: %d is a published-post count.
6226 __("%d published", "desktop-mode"),
6227 counts.posts.publish
6228 ) : ""
6229 )
6230 );
6231 cards.appendChild(
6232 buildStatCard(
6233 counts.commentsReceived.toLocaleString(),
6234 __("Comments", "desktop-mode"),
6235 ""
6236 )
6237 );
6238 cards.appendChild(
6239 buildStatCard(
6240 counts.distinctAuthors.toLocaleString(),
6241 __("Authors", "desktop-mode"),
6242 counts.distinctAuthors === 1 ? __("one contributor", "desktop-mode") : ""
6243 )
6244 );
6245 wrap.appendChild(cards);
6246 const spark = buildActivitySparkline(activity2);
6247 if (spark) {
6248 wrap.appendChild(spark);
6249 }
6250 const milestoneRow = buildTermMilestonesRow(milestones);
6251 if (milestoneRow) {
6252 wrap.appendChild(milestoneRow);
6253 }
6254 if (topAuthors.length > 0) {
6255 const section = document.createElement("section");
6256 section.className = "desktop-mode-my-wordpress__user-section";
6257 const h = document.createElement("h3");
6258 h.textContent = __("Top contributors", "desktop-mode");
6259 section.appendChild(h);
6260 const grid = document.createElement("div");
6261 grid.className = "desktop-mode-my-wordpress__term-authors";
6262 for (const a of topAuthors) {
6263 const card = document.createElement("div");
6264 card.className = "desktop-mode-my-wordpress__term-author";
6265 if (a.userAvatarUrl) {
6266 const img = document.createElement("img");
6267 img.src = a.userAvatarUrl;
6268 img.alt = "";
6269 img.className = "desktop-mode-my-wordpress__term-author-avatar";
6270 card.appendChild(img);
6271 }
6272 const text = document.createElement("div");
6273 text.className = "desktop-mode-my-wordpress__term-author-text";
6274 const name = document.createElement("span");
6275 name.className = "desktop-mode-my-wordpress__term-author-name";
6276 name.textContent = a.userName;
6277 text.appendChild(name);
6278 const count = document.createElement("span");
6279 count.className = "desktop-mode-my-wordpress__term-author-count";
6280 count.textContent = sprintf(
6281 // translators: %d is a post count.
6282 _n("%d post", "%d posts", a.count),
6283 a.count
6284 );
6285 text.appendChild(count);
6286 card.appendChild(text);
6287 grid.appendChild(card);
6288 }
6289 section.appendChild(grid);
6290 wrap.appendChild(section);
6291 }
6292 if (recent.length > 0) {
6293 const section = document.createElement("section");
6294 section.className = "desktop-mode-my-wordpress__user-section";
6295 const h = document.createElement("h3");
6296 h.textContent = __("Recent posts", "desktop-mode");
6297 section.appendChild(h);
6298 const ul = document.createElement("ul");
6299 ul.className = "desktop-mode-my-wordpress__user-recent";
6300 for (const r of recent) {
6301 const li = document.createElement("li");
6302 const a = document.createElement("a");
6303 a.href = r.link;
6304 a.target = "_blank";
6305 a.rel = "noopener noreferrer";
6306 a.textContent = r.title || `#${r.id}`;
6307 li.appendChild(a);
6308 const meta = document.createElement("span");
6309 meta.className = "desktop-mode-my-wordpress__user-recent-meta";
6310 meta.textContent = `${formatDate(r.date)} · ${r.status}${r.author?.name ? " · " + r.author.name : ""}`;
6311 li.appendChild(meta);
6312 ul.appendChild(li);
6313 }
6314 section.appendChild(ul);
6315 wrap.appendChild(section);
6316 }
6317 if (coTerms.length > 0) {
6318 const section = document.createElement("section");
6319 section.className = "desktop-mode-my-wordpress__user-section";
6320 const h = document.createElement("h3");
6321 h.textContent = profile.taxonomy === "post_tag" ? __("Often paired tags", "desktop-mode") : __("Often paired categories", "desktop-mode");
6322 section.appendChild(h);
6323 const chips = document.createElement("div");
6324 chips.className = "desktop-mode-my-wordpress__user-chips";
6325 for (const co of coTerms) {
6326 const chip = document.createElement("span");
6327 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");
6328 const name = document.createElement("span");
6329 name.textContent = co.name;
6330 chip.appendChild(name);
6331 const count = document.createElement("span");
6332 count.className = "desktop-mode-my-wordpress__user-chip-count";
6333 count.textContent = String(co.count);
6334 chip.appendChild(count);
6335 chips.appendChild(chip);
6336 }
6337 section.appendChild(chips);
6338 wrap.appendChild(section);
6339 }
6340 return wrap;
6341 }
6342 function appendTermHeader(host, header) {
6343 const wrap = document.createElement("header");
6344 wrap.className = "desktop-mode-my-wordpress__term-header";
6345 const iconHost = document.createElement("span");
6346 iconHost.className = "desktop-mode-my-wordpress__term-icon " + (header.isTag ? "desktop-mode-my-wordpress__term-icon--tag" : "desktop-mode-my-wordpress__term-icon--category");
6347 const iconGlyph = document.createElement("span");
6348 iconGlyph.style.cssText = "font-family:dashicons;font-size:32px;line-height:1;display:inline-block;";
6349 iconGlyph.textContent = header.isTag ? "" : "";
6350 iconHost.appendChild(iconGlyph);
6351 wrap.appendChild(iconHost);
6352 const right = document.createElement("div");
6353 right.className = "desktop-mode-my-wordpress__user-headline";
6354 const h = document.createElement("h2");
6355 h.className = "desktop-mode-my-wordpress__article-title";
6356 h.textContent = header.name;
6357 right.appendChild(h);
6358 const meta = document.createElement("div");
6359 meta.className = "desktop-mode-my-wordpress__user-roles";
6360 const taxBadge = document.createElement("span");
6361 taxBadge.className = "desktop-mode-my-wordpress__user-role " + (header.isTag ? "desktop-mode-my-wordpress__user-role--tag" : "desktop-mode-my-wordpress__user-role--category");
6362 taxBadge.textContent = header.taxonomyLabel;
6363 meta.appendChild(taxBadge);
6364 if (header.parentName) {
6365 const parent = document.createElement("span");
6366 parent.className = "desktop-mode-my-wordpress__user-role";
6367 parent.textContent = sprintf(
6368 // translators: %s is the name of the parent category.
6369 __("in %s", "desktop-mode"),
6370 header.parentName
6371 );
6372 meta.appendChild(parent);
6373 }
6374 right.appendChild(meta);
6375 if (header.link) {
6376 const links = document.createElement("div");
6377 links.className = "desktop-mode-my-wordpress__user-links";
6378 const a = document.createElement("a");
6379 a.href = header.link;
6380 a.target = "_blank";
6381 a.rel = "noopener noreferrer";
6382 a.textContent = __("View archive", "desktop-mode");
6383 links.appendChild(a);
6384 right.appendChild(links);
6385 }
6386 wrap.appendChild(right);
6387 host.appendChild(wrap);
6388 }
6389 function buildTermMilestonesRow(milestones) {
6390 const items = [];
6391 if (milestones.firstPosted) {
6392 items.push({
6393 label: __("First post", "desktop-mode"),
6394 value: formatYearMonth(milestones.firstPosted)
6395 });
6396 }
6397 if (milestones.lastPosted) {
6398 items.push({
6399 label: __("Last post", "desktop-mode"),
6400 value: formatYearMonth(milestones.lastPosted)
6401 });
6402 }
6403 if (items.length === 0) {
6404 return null;
6405 }
6406 const dl = document.createElement("dl");
6407 dl.className = "desktop-mode-my-wordpress__user-milestones";
6408 for (const item of items) {
6409 const dt = document.createElement("dt");
6410 dt.textContent = item.label;
6411 dl.appendChild(dt);
6412 const dd = document.createElement("dd");
6413 dd.textContent = item.value;
6414 dl.appendChild(dd);
6415 }
6416 return dl;
6417 }
6418 function mediaToView(m) {
6419 const isImage = m.mime_type.startsWith("image/");
6420 return {
6421 id: `media:${m.id}`,
6422 icon: isImage ? "dashicons-format-image" : "dashicons-media-default",
6423 label: stripTags(m.title.rendered) || `#${m.id}`,
6424 date: m.date,
6425 preview: () => {
6426 const wrap = document.createElement("div");
6427 wrap.className = "desktop-mode-my-wordpress__article";
6428 const h = document.createElement("h2");
6429 h.className = "desktop-mode-my-wordpress__article-title";
6430 h.textContent = stripTags(m.title.rendered) || `#${m.id}`;
6431 wrap.appendChild(h);
6432 const meta = document.createElement("p");
6433 meta.className = "desktop-mode-my-wordpress__article-meta";
6434 meta.textContent = `${m.mime_type} · ${formatDate(m.date)}`;
6435 wrap.appendChild(meta);
6436 if (isImage) {
6437 const img = document.createElement("img");
6438 img.className = "desktop-mode-my-wordpress__article-hero";
6439 const sizes = m.media_details?.sizes;
6440 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? m.source_url;
6441 img.alt = m.alt_text ?? "";
6442 wrap.appendChild(img);
6443 } else {
6444 const link = document.createElement("p");
6445 const a = document.createElement("a");
6446 a.href = m.source_url;
6447 a.textContent = m.source_url;
6448 a.target = "_blank";
6449 a.rel = "noopener noreferrer";
6450 link.appendChild(a);
6451 wrap.appendChild(link);
6452 }
6453 return wrap;
6454 }
6455 };
6456 }
6457 function revisionToView(r, entity, postId) {
6458 const label = stripTags(r.title?.rendered ?? "") || formatDate(r.date);
6459 return {
6460 id: `revision:${r.id}`,
6461 icon: "dashicons-backup",
6462 label,
6463 date: r.modified || r.date,
6464 preview: async () => {
6465 let detail = null;
6466 try {
6467 detail = await fetchRevision(entity, postId, r.id);
6468 } catch {
6469 detail = null;
6470 }
6471 const wrap = document.createElement("article");
6472 wrap.className = "desktop-mode-my-wordpress__article";
6473 const h = document.createElement("h2");
6474 h.className = "desktop-mode-my-wordpress__article-title";
6475 h.textContent = stripTags(detail?.title?.rendered ?? r.title?.rendered ?? "") || label;
6476 wrap.appendChild(h);
6477 const meta = document.createElement("p");
6478 meta.className = "desktop-mode-my-wordpress__article-meta";
6479 meta.textContent = sprintf(
6480 // translators: %s is a formatted date.
6481 __("Saved %s", "desktop-mode"),
6482 formatDate(detail?.modified || detail?.date || r.modified || r.date)
6483 );
6484 wrap.appendChild(meta);
6485 const html2 = detail?.content?.rendered ?? "";
6486 if (html2) {
6487 const content = document.createElement("div");
6488 content.className = "desktop-mode-my-wordpress__article-content";
6489 content.innerHTML = html2;
6490 wrap.appendChild(content);
6491 } else {
6492 const empty = document.createElement("p");
6493 empty.className = "desktop-mode-my-wordpress__article-meta";
6494 empty.textContent = detail ? __("This revision has no rendered content.", "desktop-mode") : __(
6495 "Couldn’t load the revision content. You may not have permission to view it.",
6496 "desktop-mode"
6497 );
6498 wrap.appendChild(empty);
6499 }
6500 return wrap;
6501 }
6502 };
6503 }
6504 function formatDate(iso) {
6505 if (!iso) {
6506 return "";
6507 }
6508 try {
6509 return new Date(iso).toLocaleString();
6510 } catch {
6511 return iso;
6512 }
6513 }
6514 function openEditor(entity, id, title) {
6515 const url = buildEditUrl(id);
6516 openIframeWindow({
6517 id: `${entity.id}-edit-${id}`,
6518 url,
6519 title,
6520 icon: entity.icon
6521 });
6522 }
6523 function openTileMenu(state, ctx, entity, item, title, pos) {
6524 closeAnyTileMenu();
6525 const menu = document.createElement("wpd-context-menu");
6526 menu.setAttribute("open", "");
6527 menu.classList.add("desktop-mode-my-wordpress__menu");
6528 menu.style.left = `${pos.x}px`;
6529 menu.style.top = `${pos.y}px`;
6530 const addOption = (id, label, icon, danger = false) => {
6531 const opt = document.createElement("wpd-context-menu-option");
6532 opt.dataset.menuItemId = id;
6533 opt.setAttribute("value", id);
6534 opt.setAttribute("icon", sanitizeClass(icon));
6535 if (danger) {
6536 opt.setAttribute("danger", "");
6537 }
6538 opt.textContent = label;
6539 menu.appendChild(opt);
6540 };
6541 const baseOptions = [
6542 {
6543 id: "open",
6544 label: __("Open in editor", "desktop-mode"),
6545 icon: "dashicons-edit"
6546 },
6547 {
6548 id: "navigate-into",
6549 label: __("Navigate into", "desktop-mode"),
6550 icon: "dashicons-category"
6551 },
6552 {
6553 id: "trash",
6554 label: __("Move to Trash", "desktop-mode"),
6555 icon: "dashicons-trash",
6556 danger: true
6557 }
6558 ];
6559 const ctxFilter = {
6560 entityId: entity.id,
6561 kind: entity.kind ?? "post",
6562 item
6563 };
6564 const options = applyFilters(
6565 "desktop-mode.my-wordpress.tile-context-menu",
6566 baseOptions,
6567 ctxFilter
6568 );
6569 const finalOptions = Array.isArray(options) ? options : baseOptions;
6570 for (const o of finalOptions) {
6571 addOption(o.id, o.label, o.icon, o.danger);
6572 }
6573 menu.addEventListener("wpd-context-menu-pick", (e) => {
6574 const detail = e.detail;
6575 closeAnyTileMenu();
6576 if (detail.id === "open") {
6577 openEditor(entity, item.id, title);
6578 return;
6579 }
6580 if (detail.id === "navigate-into") {
6581 navigate(state, {
6582 kind: "detail",
6583 entityId: entity.id,
6584 postId: item.id,
6585 postTitle: title
6586 });
6587 return;
6588 }
6589 if (detail.id === "trash") {
6590 void confirmTrash(state, ctx, entity, item.id, title);
6591 return;
6592 }
6593 const match = finalOptions.find((o) => o.id === detail.id);
6594 if (match && typeof match.onSelect === "function") {
6595 try {
6596 match.onSelect();
6597 } catch (err) {
6598 console.error(
6599 `[my-wordpress] tile-context-menu '${detail.id}' onSelect threw:`,
6600 err
6601 );
6602 }
6603 }
6604 });
6605 document.body.appendChild(menu);
6606 const rect = menu.getBoundingClientRect();
6607 if (rect.right > window.innerWidth) {
6608 menu.style.left = `${Math.max(
6609 0,
6610 window.innerWidth - rect.width - 8
6611 )}px`;
6612 }
6613 if (rect.bottom > window.innerHeight) {
6614 menu.style.top = `${Math.max(
6615 0,
6616 window.innerHeight - rect.height - 8
6617 )}px`;
6618 }
6619 queueMicrotask(() => {
6620 const onDocPointerDown = (ev) => {
6621 const target = ev.target;
6622 if (target instanceof Node && menu.contains(target)) {
6623 return;
6624 }
6625 closeAnyTileMenu();
6626 };
6627 const onDocKey = (ev) => {
6628 if (ev.key === "Escape") {
6629 closeAnyTileMenu();
6630 }
6631 };
6632 document.addEventListener("pointerdown", onDocPointerDown, true);
6633 document.addEventListener("keydown", onDocKey);
6634 menu.addEventListener("tile-menu-closed", () => {
6635 document.removeEventListener(
6636 "pointerdown",
6637 onDocPointerDown,
6638 true
6639 );
6640 document.removeEventListener("keydown", onDocKey);
6641 });
6642 });
6643 }
6644 function closeAnyTileMenu() {
6645 document.querySelectorAll("wpd-context-menu.desktop-mode-my-wordpress__menu").forEach((n) => {
6646 n.dispatchEvent(new CustomEvent("tile-menu-closed"));
6647 n.remove();
6648 });
6649 }
6650 async function confirmTrash(state, ctx, entity, id, title) {
6651 const ok = await wpdConfirmGlobal({
6652 title: __("Move to Trash", "desktop-mode"),
6653 message: sprintf(
6654 // translators: %s is the entry title.
6655 __('Move "%s" to Trash?', "desktop-mode"),
6656 title
6657 ),
6658 confirmLabel: __("Move to Trash", "desktop-mode"),
6659 cancelLabel: __("Cancel", "desktop-mode"),
6660 danger: true
6661 });
6662 if (!ok) {
6663 return;
6664 }
6665 try {
6666 await trashEntity(entity, id);
6667 } catch (err) {
6668 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
6669 showToast(msg);
6670 return;
6671 }
6672 const tile = ctx.tiles.querySelector(
6673 `[data-entry-id="${id}"]`
6674 );
6675 tile?.remove();
6676 if (ctx.selectedId === id) {
6677 ctx.selectedId = null;
6678 ctx.selectedTile = null;
6679 ctx.preview.replaceChildren();
6680 const empty = document.createElement("div");
6681 empty.className = "desktop-mode-my-wordpress__preview-empty";
6682 empty.textContent = __(
6683 "Select an entry to preview it here.",
6684 "desktop-mode"
6685 );
6686 ctx.preview.appendChild(empty);
6687 }
6688 }
6689 function showToast(message) {
6690 const toast = window.wp?.desktop?.toast;
6691 if (typeof toast === "function") {
6692 toast({ message });
6693 return;
6694 }
6695 console.info("[my-wordpress]", message);
6696 }
6697 function renderUserEntityList(state, entity) {
6698 const cfg = getConfig();
6699 const initialQuery = lastQueryByEntity.get(entity.id) ?? "";
6700 const toolbar = renderListToolbar({
6701 placeholder: __("Search users…", "desktop-mode"),
6702 ariaLabel: __("Search users", "desktop-mode"),
6703 initialValue: initialQuery,
6704 onSearchChange: (q) => {
6705 lastQueryByEntity.set(entity.id, q);
6706 void resetForSearch(q);
6707 }
6708 });
6709 state.body.appendChild(toolbar.host);
6710 state.teardown.push(() => toolbar.destroy());
6711 const split = document.createElement("div");
6712 split.className = "desktop-mode-my-wordpress__split";
6713 const left = document.createElement("div");
6714 left.className = "desktop-mode-my-wordpress__list";
6715 const tiles = document.createElement("div");
6716 tiles.className = "desktop-mode-my-wordpress__tiles desktop-mode-my-wordpress__canvas desktop-mode-my-wordpress__canvas--users";
6717 tiles.setAttribute("role", "list");
6718 left.appendChild(tiles);
6719 const sentinel = document.createElement("div");
6720 sentinel.className = "desktop-mode-my-wordpress__sentinel";
6721 sentinel.setAttribute("aria-hidden", "true");
6722 left.appendChild(sentinel);
6723 const right = document.createElement("div");
6724 right.className = "desktop-mode-my-wordpress__preview";
6725 const previewEmpty = document.createElement("div");
6726 previewEmpty.className = "desktop-mode-my-wordpress__preview-empty";
6727 previewEmpty.textContent = __(
6728 "Select a user to see their profile here.",
6729 "desktop-mode"
6730 );
6731 right.appendChild(previewEmpty);
6732 split.appendChild(left);
6733 split.appendChild(right);
6734 state.body.appendChild(split);
6735 const tileLayout = createTileLayout(tiles, `entity:${entity.id}`);
6736 const menu = attachIconCanvasMenu(tiles, {
6737 scope: `my-wordpress:${entity.id}`,
6738 onSort: (mode) => tileLayout.sort(mode)
6739 });
6740 state.teardown.push(() => menu.dispose());
6741 const ctx = {
6742 page: 0,
6743 totalPages: 1,
6744 total: 0,
6745 loaded: 0,
6746 loading: false,
6747 done: false,
6748 tiles,
6749 sentinel,
6750 preview: right,
6751 selectedId: null,
6752 selectedTile: null,
6753 observer: null,
6754 layout: tileLayout,
6755 query: initialQuery,
6756 abort: null
6757 };
6758 state.teardown.push(() => tileLayout.dispose());
6759 state.teardown.push(() => ctx.abort?.abort());
6760 const repaintListStatus = () => {
6761 let itemLabel;
6762 if (ctx.total === 0 && ctx.loaded === 0) {
6763 itemLabel = pluralLabel(0, "user", "users");
6764 } else if (ctx.total > ctx.loaded && ctx.loaded > 0) {
6765 itemLabel = sprintf(
6766 // translators: 1: visible user count, 2: total user count.
6767 __("%1$d of %2$d users", "desktop-mode"),
6768 ctx.loaded,
6769 ctx.total
6770 );
6771 } else {
6772 itemLabel = pluralLabel(
6773 Math.max(ctx.total, ctx.loaded),
6774 "user",
6775 "users"
6776 );
6777 }
6778 const segments = [
6779 { id: "count", label: itemLabel, align: "start", sort: 10 }
6780 ];
6781 if (ctx.totalPages > 1) {
6782 segments.push({
6783 id: "page",
6784 label: sprintf(
6785 // translators: 1: current page, 2: total pages.
6786 __("Page %1$d of %2$d", "desktop-mode"),
6787 Math.max(ctx.page, 1),
6788 ctx.totalPages
6789 ),
6790 align: "end",
6791 sort: 10
6792 });
6793 }
6794 paintStatus(state, segments, {
6795 view: "list",
6796 entityId: entity.id
6797 });
6798 };
6799 repaintListStatus();
6800 const sentinelIsVisible = () => {
6801 const sr = sentinel.getBoundingClientRect();
6802 const rr = left.getBoundingClientRect();
6803 const slack = 200;
6804 return sr.top < rr.bottom + slack && sr.bottom > rr.top - slack;
6805 };
6806 const loadMore = async () => {
6807 if (ctx.loading || ctx.done) {
6808 return;
6809 }
6810 ctx.loading = true;
6811 const nextPage = ctx.page + 1;
6812 const isFirst = nextPage === 1;
6813 const queryAtFetchTime = ctx.query;
6814 showLoadingSkeleton(tiles, ctx.layout, isFirst);
6815 const controller = new AbortController();
6816 ctx.abort = controller;
6817 try {
6818 const result = await fetchUserList(entity, {
6819 page: nextPage,
6820 perPage: cfg.perPage,
6821 search: queryAtFetchTime || void 0,
6822 signal: controller.signal
6823 });
6824 if (ctx.query !== queryAtFetchTime) {
6825 return;
6826 }
6827 ctx.page = nextPage;
6828 ctx.totalPages = result.totalPages;
6829 ctx.total = result.total;
6830 hideLoadingSkeleton(tiles);
6831 if (result.items.length === 0 && isFirst) {
6832 renderListEmptyMessage(
6833 tiles,
6834 queryAtFetchTime ? sprintf(
6835 // translators: %s is the user-entered search query.
6836 __('No users match "%s".', "desktop-mode"),
6837 queryAtFetchTime
6838 ) : __("No users to show.", "desktop-mode")
6839 );
6840 ctx.done = true;
6841 repaintListStatus();
6842 return;
6843 }
6844 for (const item of result.items) {
6845 tiles.appendChild(
6846 buildUserTile(state, ctx, entity, item)
6847 );
6848 ctx.loaded += 1;
6849 }
6850 if (ctx.page >= ctx.totalPages) {
6851 ctx.done = true;
6852 }
6853 repaintListStatus();
6854 } catch (err) {
6855 if (isAbortError(err)) {
6856 return;
6857 }
6858 hideLoadingSkeleton(tiles);
6859 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
6860 renderListError(tiles, msg);
6861 ctx.done = true;
6862 } finally {
6863 ctx.loading = false;
6864 if (ctx.abort === controller) {
6865 ctx.abort = null;
6866 }
6867 }
6868 if (!ctx.done) {
6869 requestAnimationFrame(() => {
6870 if (sentinelIsVisible()) {
6871 void loadMore();
6872 }
6873 });
6874 }
6875 };
6876 const resetForSearch = async (q) => {
6877 ctx.abort?.abort();
6878 ctx.abort = null;
6879 ctx.query = q;
6880 tiles.classList.add(
6881 "desktop-mode-my-wordpress__tiles--searching"
6882 );
6883 hideLoadingSkeleton(tiles);
6884 const controller = new AbortController();
6885 ctx.abort = controller;
6886 ctx.loading = true;
6887 try {
6888 const result = await fetchUserList(entity, {
6889 page: 1,
6890 perPage: cfg.perPage,
6891 search: q || void 0,
6892 signal: controller.signal
6893 });
6894 if (ctx.query !== q) {
6895 return;
6896 }
6897 tiles.replaceChildren();
6898 ctx.layout.clear();
6899 tiles.classList.remove(
6900 "desktop-mode-my-wordpress__tiles--searching"
6901 );
6902 ctx.page = 1;
6903 ctx.totalPages = result.totalPages;
6904 ctx.total = result.total;
6905 ctx.loaded = 0;
6906 ctx.done = ctx.page >= ctx.totalPages;
6907 ctx.selectedId = null;
6908 ctx.selectedTile = null;
6909 ctx.preview.replaceChildren();
6910 const emptyPreview = document.createElement("div");
6911 emptyPreview.className = "desktop-mode-my-wordpress__preview-empty";
6912 emptyPreview.textContent = __(
6913 "Select a user to see their profile here.",
6914 "desktop-mode"
6915 );
6916 ctx.preview.appendChild(emptyPreview);
6917 if (result.items.length === 0) {
6918 renderListEmptyMessage(
6919 tiles,
6920 q ? sprintf(
6921 // translators: %s is the user-entered search query.
6922 __('No users match "%s".', "desktop-mode"),
6923 q
6924 ) : __("No users to show.", "desktop-mode")
6925 );
6926 ctx.done = true;
6927 } else {
6928 for (const item of result.items) {
6929 tiles.appendChild(
6930 buildUserTile(state, ctx, entity, item)
6931 );
6932 ctx.loaded += 1;
6933 }
6934 }
6935 repaintListStatus();
6936 } catch (err) {
6937 if (isAbortError(err)) {
6938 return;
6939 }
6940 tiles.classList.remove(
6941 "desktop-mode-my-wordpress__tiles--searching"
6942 );
6943 tiles.replaceChildren();
6944 ctx.layout.clear();
6945 const msg = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
6946 renderListError(tiles, msg);
6947 ctx.done = true;
6948 } finally {
6949 ctx.loading = false;
6950 if (ctx.abort === controller) {
6951 ctx.abort = null;
6952 }
6953 }
6954 if (!ctx.done) {
6955 requestAnimationFrame(() => {
6956 if (sentinelIsVisible()) {
6957 void loadMore();
6958 }
6959 });
6960 }
6961 };
6962 if (typeof IntersectionObserver !== "undefined") {
6963 ctx.observer = new IntersectionObserver(
6964 (entries) => {
6965 for (const e of entries) {
6966 if (e.isIntersecting) {
6967 void loadMore();
6968 }
6969 }
6970 },
6971 { root: left, rootMargin: "200px 0px" }
6972 );
6973 ctx.observer.observe(sentinel);
6974 state.teardown.push(() => ctx.observer?.disconnect());
6975 }
6976 void loadMore();
6977 }
6978 function buildUserTile(state, ctx, entity, item) {
6979 const displayName = item.name || item.slug || `#${item.id}`;
6980 const avatarUrl = pickAvatar(item.avatar_urls) ?? "";
6981 const tile = buildTileFromSpec({
6982 type: "user",
6983 ref: String(item.id),
6984 label: displayName,
6985 thumbnail: avatarUrl || void 0,
6986 // No avatar: fall back to a generic users dashicon so the
6987 // tile still has a visual. The initials block below
6988 // replaces that icon as a richer fallback.
6989 icon: avatarUrl ? void 0 : "dashicons-admin-users",
6990 role: "entry",
6991 dataset: { userId: item.id, role: "user" },
6992 extraClasses: [
6993 "desktop-mode-my-wordpress__tile",
6994 "desktop-mode-my-wordpress__tile--user"
6995 ]
6996 });
6997 if (!avatarUrl) {
6998 const iconHost = tile.querySelector(
6999 ".desktop-mode-file-tile__visual"
7000 );
7001 if (iconHost) {
7002 iconHost.replaceChildren();
7003 const initials = document.createElement("span");
7004 initials.className = "desktop-mode-my-wordpress__user-tile-initials";
7005 initials.textContent = initialsOf(displayName);
7006 iconHost.appendChild(initials);
7007 }
7008 }
7009 const summary = item.desktop_mode_summary;
7010 const postCount = summary?.postCount ?? 0;
7011 const roleLabel = (summary?.roleLabels ?? [])[0] ?? "";
7012 if (roleLabel || postCount > 0) {
7013 const sub = document.createElement("span");
7014 sub.className = "desktop-mode-my-wordpress__user-tile-sub";
7015 const parts = [];
7016 if (roleLabel) {
7017 parts.push(roleLabel);
7018 }
7019 if (postCount > 0) {
7020 parts.push(
7021 sprintf(
7022 // translators: %d is a count of posts authored.
7023 _n("%d post", "%d posts", postCount),
7024 postCount
7025 )
7026 );
7027 }
7028 sub.textContent = parts.join(" · ");
7029 tile.appendChild(sub);
7030 }
7031 const tooltip = buildUserTooltip(displayName, item);
7032 let tooltipNode = null;
7033 const showTooltip = (ev) => {
7034 if (!tooltipNode) {
7035 tooltipNode = tooltip;
7036 }
7037 document.body.appendChild(tooltipNode);
7038 positionTooltip(tooltipNode, ev);
7039 };
7040 const moveTooltip = (ev) => {
7041 if (tooltipNode && tooltipNode.isConnected) {
7042 positionTooltip(tooltipNode, ev);
7043 }
7044 };
7045 const hideTooltip = () => {
7046 if (tooltipNode && tooltipNode.isConnected) {
7047 tooltipNode.remove();
7048 }
7049 };
7050 tile.addEventListener("mouseenter", showTooltip);
7051 tile.addEventListener("mousemove", moveTooltip);
7052 tile.addEventListener("mouseleave", hideTooltip);
7053 state.teardown.push(hideTooltip);
7054 attachTileDragOut(
7055 tile,
7056 {
7057 kind: "user",
7058 ref: String(item.id),
7059 title: displayName,
7060 icon: "dashicons-admin-users",
7061 // Cross-frame bridge payload — receiver inserts a
7062 // `core/paragraph` with `<a href>` pointing at the
7063 // author archive (`item.link`). Falls back to empty
7064 // string when the REST shape omitted the link; the
7065 // receiver gates on a truthy URL.
7066 bridgePayload: {
7067 kind: "user",
7068 id: item.id,
7069 url: item.link ?? "",
7070 title: displayName
7071 }
7072 },
7073 () => hideTooltip()
7074 );
7075 const tileKey = `entry:${item.id}`;
7076 ctx.layout.place(tile, tileKey, {
7077 name: displayName,
7078 // Order users by post count by default — the most active
7079 // surface first. Authoring date isn't available per-user,
7080 // so we synthesize a date that ranks more-prolific users
7081 // earlier when the canvas sort-by-date is selected.
7082 date: postCount > 0 ? new Date(2100, 0, 1 - postCount).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString()
7083 });
7084 tile.addEventListener("click", () => {
7085 selectUserTile(state, ctx, tile, item);
7086 });
7087 tile.addEventListener("dblclick", (e) => {
7088 e.preventDefault();
7089 hideTooltip();
7090 navigate(state, {
7091 kind: "user-footprint",
7092 entityId: entity.id,
7093 userId: item.id,
7094 userName: displayName
7095 });
7096 });
7097 tile.addEventListener("contextmenu", (e) => {
7098 e.preventDefault();
7099 hideTooltip();
7100 openUserTileMenu(state, entity, item, displayName, {
7101 x: e.clientX,
7102 y: e.clientY
7103 });
7104 });
7105 return tile;
7106 }
7107 function buildUserTooltip(name, item) {
7108 const tip = document.createElement("div");
7109 tip.className = "desktop-mode-my-wordpress__tooltip";
7110 tip.setAttribute("role", "tooltip");
7111 const heading = document.createElement("div");
7112 heading.className = "desktop-mode-my-wordpress__tooltip-title";
7113 heading.textContent = name;
7114 tip.appendChild(heading);
7115 const summary = item.desktop_mode_summary;
7116 const roleLabel = (summary?.roleLabels ?? [])[0];
7117 const postCount = summary?.postCount ?? 0;
7118 const lastActive = summary?.lastActive ?? "";
7119 const lines = [];
7120 if (roleLabel) {
7121 lines.push(roleLabel);
7122 }
7123 if (postCount > 0) {
7124 lines.push(
7125 sprintf(
7126 // translators: %d is a count of posts authored by a user.
7127 _n("%d post", "%d posts", postCount),
7128 postCount
7129 )
7130 );
7131 }
7132 if (lastActive) {
7133 lines.push(
7134 sprintf(
7135 // translators: %s is a relative or absolute date.
7136 __("Last published %s", "desktop-mode"),
7137 formatDate(lastActive)
7138 )
7139 );
7140 }
7141 for (const ln of lines) {
7142 const p = document.createElement("p");
7143 p.className = "desktop-mode-my-wordpress__tooltip-excerpt";
7144 p.textContent = ln;
7145 tip.appendChild(p);
7146 }
7147 const bio = (item.description ?? "").trim();
7148 if (bio) {
7149 const p = document.createElement("p");
7150 p.className = "desktop-mode-my-wordpress__tooltip-excerpt";
7151 p.textContent = bio.length > 200 ? bio.slice(0, 197) + "" : bio;
7152 tip.appendChild(p);
7153 }
7154 return tip;
7155 }
7156 function selectUserTile(state, ctx, tile, item) {
7157 if (ctx.selectedTile) {
7158 ctx.selectedTile.classList.remove(
7159 "desktop-mode-file-tile--selected"
7160 );
7161 }
7162 tile.classList.add("desktop-mode-file-tile--selected");
7163 ctx.selectedTile = tile;
7164 ctx.selectedId = item.id;
7165 void renderUserPreviewPane(state, ctx, item);
7166 }
7167 async function renderUserPreviewPane(state, ctx, item) {
7168 const fallbackName = item.name || item.slug || `#${item.id}`;
7169 const fallbackAvatar = pickAvatar(item.avatar_urls) ?? "";
7170 const userId = item.id;
7171 showPreviewLoading(ctx.preview);
7172 let node;
7173 try {
7174 node = await renderUserDossier({
7175 userId,
7176 fallbackName,
7177 fallbackAvatar,
7178 fallbackDescription: item.description ?? ""
7179 });
7180 } catch (err) {
7181 if (ctx.selectedId !== userId) {
7182 return;
7183 }
7184 showPreviewError(ctx.preview, err);
7185 return;
7186 }
7187 if (ctx.selectedId !== userId) {
7188 return;
7189 }
7190 const footer = document.createElement("footer");
7191 footer.className = "desktop-mode-my-wordpress__article-footer";
7192 const footprintBtn = document.createElement("wpd-button");
7193 footprintBtn.setAttribute("variant", "primary");
7194 footprintBtn.textContent = __("View activity footprint", "desktop-mode");
7195 footprintBtn.title = __(
7196 "Open the full activity footprint surface for this user.",
7197 "desktop-mode"
7198 );
7199 footprintBtn.addEventListener("click", () => {
7200 navigate(state, {
7201 kind: "user-footprint",
7202 entityId: "users",
7203 userId,
7204 userName: fallbackName
7205 });
7206 });
7207 footer.appendChild(footprintBtn);
7208 const editBtn = document.createElement("wpd-button");
7209 editBtn.setAttribute("variant", "secondary");
7210 editBtn.textContent = __("Show profile", "desktop-mode");
7211 editBtn.title = __(
7212 "Open this user’s profile editor in a new window.",
7213 "desktop-mode"
7214 );
7215 editBtn.addEventListener("click", () => {
7216 openUserEditWindow(userId);
7217 });
7218 footer.appendChild(editBtn);
7219 node.appendChild(footer);
7220 ctx.preview.replaceChildren(node);
7221 }
7222 function openUserTileMenu(state, entity, item, name, pos) {
7223 closeAnyTileMenu();
7224 const menu = document.createElement("wpd-context-menu");
7225 menu.setAttribute("open", "");
7226 menu.classList.add("desktop-mode-my-wordpress__menu");
7227 menu.style.left = `${pos.x}px`;
7228 menu.style.top = `${pos.y}px`;
7229 const addOption = (id, label, icon) => {
7230 const opt = document.createElement("wpd-context-menu-option");
7231 opt.dataset.menuItemId = id;
7232 opt.setAttribute("value", id);
7233 opt.setAttribute("icon", sanitizeClass(icon));
7234 opt.textContent = label;
7235 menu.appendChild(opt);
7236 };
7237 addOption(
7238 "footprint",
7239 __("View activity footprint", "desktop-mode"),
7240 "dashicons-chart-area"
7241 );
7242 addOption(
7243 "open-profile",
7244 __("Show profile", "desktop-mode"),
7245 "dashicons-id-alt"
7246 );
7247 if (item.link) {
7248 addOption(
7249 "author-archive",
7250 __("View author archive", "desktop-mode"),
7251 "dashicons-external"
7252 );
7253 }
7254 menu.addEventListener("wpd-context-menu-pick", (e) => {
7255 const detail = e.detail;
7256 closeAnyTileMenu();
7257 if (detail.id === "footprint") {
7258 navigate(state, {
7259 kind: "user-footprint",
7260 entityId: entity.id,
7261 userId: item.id,
7262 userName: name
7263 });
7264 return;
7265 }
7266 if (detail.id === "open-profile") {
7267 openUserEditWindow(item.id);
7268 return;
7269 }
7270 if (detail.id === "author-archive" && item.link) {
7271 window.open(item.link, "_blank", "noopener,noreferrer");
7272 }
7273 });
7274 document.body.appendChild(menu);
7275 const rect = menu.getBoundingClientRect();
7276 if (rect.right > window.innerWidth) {
7277 menu.style.left = `${Math.max(
7278 0,
7279 window.innerWidth - rect.width - 8
7280 )}px`;
7281 }
7282 if (rect.bottom > window.innerHeight) {
7283 menu.style.top = `${Math.max(
7284 0,
7285 window.innerHeight - rect.height - 8
7286 )}px`;
7287 }
7288 queueMicrotask(() => {
7289 const onDocPointerDown = (ev) => {
7290 const target = ev.target;
7291 if (target instanceof Node && menu.contains(target)) {
7292 return;
7293 }
7294 closeAnyTileMenu();
7295 };
7296 const onDocKey = (ev) => {
7297 if (ev.key === "Escape") {
7298 closeAnyTileMenu();
7299 }
7300 };
7301 document.addEventListener("pointerdown", onDocPointerDown, true);
7302 document.addEventListener("keydown", onDocKey);
7303 menu.addEventListener("tile-menu-closed", () => {
7304 document.removeEventListener(
7305 "pointerdown",
7306 onDocPointerDown,
7307 true
7308 );
7309 document.removeEventListener("keydown", onDocKey);
7310 });
7311 });
7312 }
7313 function openUserEditWindow(userId) {
7314 if (!Number.isFinite(userId) || userId <= 0) {
7315 return;
7316 }
7317 const desktop = window.wp?.desktop;
7318 const createSharedStore = desktop?.createSharedStore;
7319 if (typeof createSharedStore === "function") {
7320 const store = createSharedStore(
7321 "desktop-mode/user-edit/target",
7322 () => ({ userId: null, requestedAt: 0, tabRequested: false })
7323 );
7324 store.state.userId = userId;
7325 store.state.requestedAt = Date.now();
7326 store.state.tabRequested = true;
7327 store.notify();
7328 }
7329 const opened = desktop?.openWindow?.("desktop-mode-user-edit", {
7330 source: "my-wordpress/user-tile"
7331 });
7332 if (!opened) {
7333 openIframeWindow({
7334 id: `user-edit-${userId}`,
7335 url: buildEditUserUrl(userId),
7336 title: __("Edit user", "desktop-mode"),
7337 icon: "dashicons-admin-users"
7338 });
7339 }
7340 }
7341 function initialsOf(name) {
7342 const parts = name.trim().split(/\s+/).filter((s) => s.length > 0);
7343 if (parts.length === 0) {
7344 return "?";
7345 }
7346 if (parts.length === 1) {
7347 return parts[0].slice(0, 2).toUpperCase();
7348 }
7349 return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
7350 }
7351 function renderUserFootprint(state, entity, userId, userName) {
7352 const host = document.createElement("div");
7353 host.className = "desktop-mode-my-wordpress__footprint";
7354 state.body.appendChild(host);
7355 showPreviewLoading(host);
7356 paintStatus(
7357 state,
7358 [
7359 {
7360 id: "loading",
7361 label: __("Loading footprint…", "desktop-mode"),
7362 align: "start",
7363 sort: 10
7364 }
7365 ],
7366 { view: "detail", entityId: entity.id, postId: userId }
7367 );
7368 void (async () => {
7369 let payload;
7370 try {
7371 payload = await fetchUserFootprint(userId);
7372 } catch (err) {
7373 showPreviewError(host, err);
7374 paintStatus(
7375 state,
7376 [
7377 {
7378 id: "error",
7379 label: __("Could not load footprint.", "desktop-mode"),
7380 align: "start",
7381 sort: 10
7382 }
7383 ],
7384 { view: "detail", entityId: entity.id, postId: userId }
7385 );
7386 return;
7387 }
7388 if (state.route.kind !== "user-footprint" || state.route.userId !== userId) {
7389 return;
7390 }
7391 host.replaceChildren();
7392 host.appendChild(buildFootprintHero(payload));
7393 host.appendChild(buildFootprintHeadlineStats(payload));
7394 host.appendChild(buildFootprintCalendar(payload));
7395 host.appendChild(buildFootprintRhythm(payload));
7396 const monthCallout = buildFootprintMonthCallout(payload);
7397 if (monthCallout) {
7398 host.appendChild(monthCallout);
7399 }
7400 host.appendChild(buildFootprintTimeline(payload));
7401 host.appendChild(
7402 buildFootprintFooter(payload, userId)
7403 );
7404 paintStatus(
7405 state,
7406 [
7407 {
7408 id: "count",
7409 label: sprintf(
7410 // translators: 1: post total, 2: comment total.
7411 __(
7412 "%1$d posts · %2$d comments tracked",
7413 "desktop-mode"
7414 ),
7415 payload.totals.posts + payload.totals.pages,
7416 payload.totals.comments
7417 ),
7418 align: "start",
7419 sort: 10
7420 },
7421 {
7422 id: "range",
7423 label: sprintf(
7424 // translators: 1: window-start date, 2: window-end date.
7425 __(
7426 "Window %1$s → %2$s",
7427 "desktop-mode"
7428 ),
7429 formatShortDate(payload.range.from),
7430 formatShortDate(payload.range.to)
7431 ),
7432 align: "end",
7433 sort: 10
7434 }
7435 ],
7436 { view: "detail", entityId: entity.id, postId: userId }
7437 );
7438 })();
7439 }
7440 function buildFootprintHero(payload) {
7441 const hero = document.createElement("header");
7442 hero.className = "desktop-mode-my-wordpress__footprint-hero";
7443 const avatar = document.createElement("div");
7444 avatar.className = "desktop-mode-my-wordpress__footprint-avatar";
7445 if (payload.profile.avatarUrl) {
7446 const img = document.createElement("img");
7447 img.src = payload.profile.avatarUrl;
7448 img.alt = "";
7449 avatar.appendChild(img);
7450 } else {
7451 const span = document.createElement("span");
7452 span.className = "desktop-mode-my-wordpress__user-tile-initials";
7453 span.textContent = initialsOf(payload.profile.name);
7454 avatar.appendChild(span);
7455 }
7456 hero.appendChild(avatar);
7457 const text = document.createElement("div");
7458 text.className = "desktop-mode-my-wordpress__footprint-headline";
7459 const h = document.createElement("h1");
7460 h.className = "desktop-mode-my-wordpress__footprint-title";
7461 h.textContent = payload.profile.name;
7462 text.appendChild(h);
7463 const meta = document.createElement("div");
7464 meta.className = "desktop-mode-my-wordpress__footprint-meta";
7465 const roles = payload.profile.roleLabels ?? [];
7466 for (const r of roles) {
7467 const chip = document.createElement("span");
7468 chip.className = "desktop-mode-my-wordpress__user-role";
7469 chip.textContent = r;
7470 meta.appendChild(chip);
7471 }
7472 if (payload.profile.registered) {
7473 const since = document.createElement("span");
7474 since.className = "desktop-mode-my-wordpress__user-role desktop-mode-my-wordpress__footprint-since";
7475 since.textContent = sprintf(
7476 // translators: %s is a year-month label like "January 2023".
7477 __("Member since %s", "desktop-mode"),
7478 formatYearMonth(payload.profile.registered)
7479 );
7480 meta.appendChild(since);
7481 }
7482 text.appendChild(meta);
7483 if (payload.profile.link) {
7484 const links = document.createElement("div");
7485 links.className = "desktop-mode-my-wordpress__user-links";
7486 const a = document.createElement("a");
7487 a.href = payload.profile.link;
7488 a.target = "_blank";
7489 a.rel = "noopener noreferrer";
7490 a.textContent = __("Author archive", "desktop-mode");
7491 links.appendChild(a);
7492 text.appendChild(links);
7493 }
7494 hero.appendChild(text);
7495 return hero;
7496 }
7497 function buildFootprintHeadlineStats(payload) {
7498 const wrap = document.createElement("section");
7499 wrap.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-stats-row";
7500 const totalContent = payload.totals.posts + payload.totals.pages;
7501 wrap.appendChild(
7502 buildStatCard(
7503 totalContent.toLocaleString(),
7504 __("Total content", "desktop-mode"),
7505 payload.totals.posts > 0 && payload.totals.pages > 0 ? sprintf(
7506 // translators: 1: post count, 2: page count.
7507 __(
7508 "%1$d posts · %2$d pages",
7509 "desktop-mode"
7510 ),
7511 payload.totals.posts,
7512 payload.totals.pages
7513 ) : ""
7514 )
7515 );
7516 wrap.appendChild(
7517 buildStatCard(
7518 payload.totals.comments.toLocaleString(),
7519 __("Comments left", "desktop-mode"),
7520 ""
7521 )
7522 );
7523 const updateCount = payload.totals.updates ?? 0;
7524 if (updateCount > 0) {
7525 wrap.appendChild(
7526 buildStatCard(
7527 updateCount.toLocaleString(),
7528 __("Updates", "desktop-mode"),
7529 __("Saves on existing posts", "desktop-mode")
7530 )
7531 );
7532 }
7533 const longestRange = payload.streak.longestRange;
7534 const longestCaption = longestRange.from && longestRange.to ? sprintf(
7535 // translators: 1: start date, 2: end date.
7536 __("%1$s → %2$s", "desktop-mode"),
7537 formatShortDate(longestRange.from),
7538 formatShortDate(longestRange.to)
7539 ) : "";
7540 wrap.appendChild(
7541 buildStatCard(
7542 sprintf(
7543 // translators: %d is the length in days of the user's longest publishing streak.
7544 _n(
7545 "%d day",
7546 "%d days",
7547 payload.streak.longest
7548 ),
7549 payload.streak.longest
7550 ),
7551 __("Longest streak", "desktop-mode"),
7552 longestCaption
7553 )
7554 );
7555 wrap.appendChild(
7556 buildStatCard(
7557 sprintf(
7558 // translators: %d is the length in days of the user's current active streak.
7559 _n("%d day", "%d days", payload.streak.current),
7560 payload.streak.current
7561 ),
7562 __("Current streak", "desktop-mode"),
7563 payload.streak.current === 0 ? __("No activity today", "desktop-mode") : __("Including today", "desktop-mode")
7564 )
7565 );
7566 return wrap;
7567 }
7568 function buildFootprintCalendar(payload) {
7569 const section = document.createElement("section");
7570 section.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-calendar-section";
7571 const h = document.createElement("h3");
7572 h.textContent = __("A year of activity", "desktop-mode");
7573 section.appendChild(h);
7574 const calendar = document.createElement("div");
7575 calendar.className = "desktop-mode-my-wordpress__footprint-calendar";
7576 const dayIntensity = (d) => d.posts + d.comments + (d.updates ?? 0);
7577 const maxIntensity = payload.daily.reduce((m, d) => {
7578 const v = dayIntensity(d);
7579 return v > m ? v : m;
7580 }, 0);
7581 const bucketize = (v) => {
7582 if (v <= 0) {
7583 return 0;
7584 }
7585 if (maxIntensity <= 0) {
7586 return 0;
7587 }
7588 const ratio = v / maxIntensity;
7589 if (ratio > 0.75) {
7590 return 4;
7591 }
7592 if (ratio > 0.5) {
7593 return 3;
7594 }
7595 if (ratio > 0.25) {
7596 return 2;
7597 }
7598 return 1;
7599 };
7600 const dates = payload.daily.map((d) => /* @__PURE__ */ new Date(d.date + "T00:00:00Z"));
7601 if (dates.length === 0) {
7602 const empty = document.createElement("p");
7603 empty.className = "desktop-mode-my-wordpress__article-meta";
7604 empty.textContent = __(
7605 "No activity recorded in the last year.",
7606 "desktop-mode"
7607 );
7608 section.appendChild(empty);
7609 return section;
7610 }
7611 const firstDow = dates[0].getUTCDay();
7612 const grid = document.createElement("div");
7613 grid.className = "desktop-mode-my-wordpress__footprint-grid";
7614 const placeCell = (el, linearDayOffset) => {
7615 const dow = linearDayOffset % 7;
7616 const week = Math.floor(linearDayOffset / 7);
7617 el.style.gridRow = String(dow + 2);
7618 el.style.gridColumn = String(week + 2);
7619 };
7620 const weekdaySource = [
7621 // 2024-12-02 was a Monday (UTC).
7622 new Date(Date.UTC(2024, 11, 2)),
7623 // Mon
7624 new Date(Date.UTC(2024, 11, 4)),
7625 // Wed
7626 new Date(Date.UTC(2024, 11, 6))
7627 // Fri
7628 ];
7629 const weekdayRows = [2, 4, 6];
7630 for (let i = 0; i < weekdaySource.length; i += 1) {
7631 const lbl = document.createElement("span");
7632 lbl.className = "desktop-mode-my-wordpress__footprint-weekday";
7633 lbl.textContent = weekdaySource[i].toLocaleDateString(void 0, {
7634 weekday: "short"
7635 });
7636 lbl.style.gridColumn = "1";
7637 lbl.style.gridRow = String(weekdayRows[i] + 1);
7638 grid.appendChild(lbl);
7639 }
7640 for (let i = 0; i < firstDow; i += 1) {
7641 const blank = document.createElement("span");
7642 blank.className = "desktop-mode-my-wordpress__footprint-cell desktop-mode-my-wordpress__footprint-cell--pad";
7643 blank.setAttribute("aria-hidden", "true");
7644 placeCell(blank, i);
7645 grid.appendChild(blank);
7646 }
7647 let lastMonth = -1;
7648 for (let i = 0; i < payload.daily.length; i += 1) {
7649 const d = dates[i];
7650 const m = d.getUTCMonth();
7651 if (m === lastMonth) {
7652 continue;
7653 }
7654 lastMonth = m;
7655 const linear = firstDow + i;
7656 const week = Math.floor(linear / 7);
7657 if (week === 0 && linear % 7 !== 0) {
7658 continue;
7659 }
7660 const lbl = document.createElement("span");
7661 lbl.className = "desktop-mode-my-wordpress__footprint-month";
7662 lbl.textContent = d.toLocaleDateString(void 0, { month: "short" });
7663 lbl.style.gridRow = "1";
7664 lbl.style.gridColumn = String(week + 2);
7665 grid.appendChild(lbl);
7666 }
7667 for (let i = 0; i < payload.daily.length; i += 1) {
7668 const d = payload.daily[i];
7669 const intensity = bucketize(dayIntensity(d));
7670 const cell = document.createElement("span");
7671 cell.className = `desktop-mode-my-wordpress__footprint-cell desktop-mode-my-wordpress__footprint-cell--l${intensity}`;
7672 cell.title = sprintf(
7673 // translators: 1: date, 2: post count, 3: comment count, 4: update (re-save) count.
7674 __(
7675 "%1$s — %2$d posts, %3$d comments, %4$d updates",
7676 "desktop-mode"
7677 ),
7678 formatLongDate(d.date),
7679 d.posts,
7680 d.comments,
7681 d.updates ?? 0
7682 );
7683 cell.dataset.date = d.date;
7684 placeCell(cell, firstDow + i);
7685 grid.appendChild(cell);
7686 }
7687 calendar.appendChild(grid);
7688 const legend = document.createElement("div");
7689 legend.className = "desktop-mode-my-wordpress__footprint-legend";
7690 const less = document.createElement("span");
7691 less.className = "desktop-mode-my-wordpress__footprint-legend-label";
7692 less.textContent = __("Less", "desktop-mode");
7693 legend.appendChild(less);
7694 for (let i = 0; i <= 4; i += 1) {
7695 const sw = document.createElement("span");
7696 sw.className = `desktop-mode-my-wordpress__footprint-cell desktop-mode-my-wordpress__footprint-cell--l${i}`;
7697 legend.appendChild(sw);
7698 }
7699 const more = document.createElement("span");
7700 more.className = "desktop-mode-my-wordpress__footprint-legend-label";
7701 more.textContent = __("More", "desktop-mode");
7702 legend.appendChild(more);
7703 calendar.appendChild(legend);
7704 section.appendChild(calendar);
7705 return section;
7706 }
7707 function buildFootprintRhythm(payload) {
7708 const section = document.createElement("section");
7709 section.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-rhythm";
7710 const h = document.createElement("h3");
7711 h.textContent = __("Publishing rhythm", "desktop-mode");
7712 section.appendChild(h);
7713 const grid = document.createElement("div");
7714 grid.className = "desktop-mode-my-wordpress__footprint-rhythm-grid";
7715 const weekdayWrap = document.createElement("div");
7716 weekdayWrap.className = "desktop-mode-my-wordpress__footprint-chart";
7717 const weekdayCap = document.createElement("div");
7718 weekdayCap.className = "desktop-mode-my-wordpress__footprint-chart-caption";
7719 weekdayCap.textContent = __("By weekday", "desktop-mode");
7720 weekdayWrap.appendChild(weekdayCap);
7721 const weekdayLabels = [
7722 __("S", "desktop-mode"),
7723 __("M", "desktop-mode"),
7724 __("T", "desktop-mode"),
7725 __("W", "desktop-mode"),
7726 __("T", "desktop-mode"),
7727 __("F", "desktop-mode"),
7728 __("S", "desktop-mode")
7729 ];
7730 const weekdayFull = [
7731 __("Sunday", "desktop-mode"),
7732 __("Monday", "desktop-mode"),
7733 __("Tuesday", "desktop-mode"),
7734 __("Wednesday", "desktop-mode"),
7735 __("Thursday", "desktop-mode"),
7736 __("Friday", "desktop-mode"),
7737 __("Saturday", "desktop-mode")
7738 ];
7739 weekdayWrap.appendChild(
7740 buildBarChart(payload.weekday, weekdayLabels, weekdayFull)
7741 );
7742 grid.appendChild(weekdayWrap);
7743 const hourWrap = document.createElement("div");
7744 hourWrap.className = "desktop-mode-my-wordpress__footprint-chart";
7745 const hourCap = document.createElement("div");
7746 hourCap.className = "desktop-mode-my-wordpress__footprint-chart-caption";
7747 hourCap.textContent = __("By hour of day (site time)", "desktop-mode");
7748 hourWrap.appendChild(hourCap);
7749 const hourLabels = [
7750 "0",
7751 "",
7752 "",
7753 "3",
7754 "",
7755 "",
7756 "6",
7757 "",
7758 "",
7759 "9",
7760 "",
7761 "",
7762 "12",
7763 "",
7764 "",
7765 "15",
7766 "",
7767 "",
7768 "18",
7769 "",
7770 "",
7771 "21",
7772 "",
7773 ""
7774 ];
7775 const hourFull = Array.from(
7776 { length: 24 },
7777 (_, i) => sprintf(
7778 // translators: %d is an hour of the day (0-23).
7779 __("%d:00", "desktop-mode"),
7780 i
7781 )
7782 );
7783 hourWrap.appendChild(
7784 buildBarChart(payload.hour, hourLabels, hourFull)
7785 );
7786 grid.appendChild(hourWrap);
7787 section.appendChild(grid);
7788 return section;
7789 }
7790 function buildBarChart(values, labels, titles) {
7791 const chart = document.createElement("div");
7792 chart.className = "desktop-mode-my-wordpress__footprint-bars";
7793 const max = Math.max(1, ...values);
7794 values.forEach((v, i) => {
7795 const col = document.createElement("div");
7796 col.className = "desktop-mode-my-wordpress__footprint-bar-col";
7797 const bar = document.createElement("div");
7798 bar.className = "desktop-mode-my-wordpress__footprint-bar";
7799 bar.style.height = `${Math.round(v / max * 100)}%`;
7800 bar.title = sprintf(
7801 // translators: 1: bucket label, 2: count.
7802 __(
7803 "%1$s · %2$d",
7804 "desktop-mode"
7805 ),
7806 titles[i] ?? labels[i] ?? String(i),
7807 v
7808 );
7809 if (v === 0) {
7810 bar.classList.add(
7811 "desktop-mode-my-wordpress__footprint-bar--empty"
7812 );
7813 }
7814 col.appendChild(bar);
7815 const lbl = document.createElement("span");
7816 lbl.className = "desktop-mode-my-wordpress__footprint-bar-label";
7817 lbl.textContent = labels[i] ?? "";
7818 col.appendChild(lbl);
7819 chart.appendChild(col);
7820 });
7821 return chart;
7822 }
7823 function buildFootprintMonthCallout(payload) {
7824 const m = payload.totals.mostProlificMonth;
7825 if (!m) {
7826 return null;
7827 }
7828 const section = document.createElement("section");
7829 section.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-callout";
7830 const label = document.createElement("span");
7831 label.className = "desktop-mode-my-wordpress__footprint-callout-label";
7832 label.textContent = __("Most prolific month", "desktop-mode");
7833 section.appendChild(label);
7834 const value = document.createElement("h3");
7835 value.className = "desktop-mode-my-wordpress__footprint-callout-value";
7836 value.textContent = formatYearMonth(m.ym + "-01T00:00:00Z");
7837 section.appendChild(value);
7838 const detail = document.createElement("p");
7839 detail.className = "desktop-mode-my-wordpress__footprint-callout-detail";
7840 detail.textContent = sprintf(
7841 // translators: %d is a post count.
7842 _n(
7843 "%d post published — their personal record.",
7844 "%d posts published — their personal record.",
7845 m.n
7846 ),
7847 m.n
7848 );
7849 section.appendChild(detail);
7850 return section;
7851 }
7852 function buildFootprintTimeline(payload) {
7853 const section = document.createElement("section");
7854 section.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-timeline-section";
7855 const h = document.createElement("h3");
7856 h.textContent = __("Recent activity", "desktop-mode");
7857 section.appendChild(h);
7858 if (payload.timeline.length === 0) {
7859 const empty = document.createElement("p");
7860 empty.className = "desktop-mode-my-wordpress__article-meta";
7861 empty.textContent = __("Nothing to show yet.", "desktop-mode");
7862 section.appendChild(empty);
7863 return section;
7864 }
7865 const list = document.createElement("ul");
7866 list.className = "desktop-mode-my-wordpress__footprint-timeline";
7867 for (const ev of payload.timeline) {
7868 const li = document.createElement("li");
7869 li.className = `desktop-mode-my-wordpress__footprint-event desktop-mode-my-wordpress__footprint-event--${ev.kind}`;
7870 const dot = document.createElement("span");
7871 dot.className = "desktop-mode-my-wordpress__footprint-dot";
7872 const icon = document.createElement("span");
7873 let iconClass = "dashicons-admin-post";
7874 if (ev.kind === "comment") {
7875 iconClass = "dashicons-admin-comments";
7876 } else if (ev.kind === "post-update") {
7877 iconClass = "dashicons-edit";
7878 }
7879 icon.className = "dashicons " + iconClass;
7880 icon.setAttribute("aria-hidden", "true");
7881 dot.appendChild(icon);
7882 li.appendChild(dot);
7883 const body = document.createElement("div");
7884 body.className = "desktop-mode-my-wordpress__footprint-event-body";
7885 const title = ev.title || __("(no title)", "desktop-mode");
7886 const titleNode = ev.link ? document.createElement("a") : document.createElement("span");
7887 titleNode.className = "desktop-mode-my-wordpress__footprint-event-title";
7888 if (ev.kind === "comment") {
7889 titleNode.textContent = sprintf(
7890 // translators: %s is a post title the user commented on.
7891 __("Commented on “%s”", "desktop-mode"),
7892 title
7893 );
7894 } else if (ev.kind === "post-update") {
7895 titleNode.textContent = sprintf(
7896 // translators: %s is the post title the user re-saved.
7897 __("Updated “%s”", "desktop-mode"),
7898 title
7899 );
7900 } else {
7901 titleNode.textContent = title;
7902 }
7903 if (ev.link && titleNode instanceof HTMLAnchorElement) {
7904 titleNode.href = ev.link;
7905 titleNode.target = "_blank";
7906 titleNode.rel = "noopener noreferrer";
7907 }
7908 body.appendChild(titleNode);
7909 const meta = document.createElement("span");
7910 meta.className = "desktop-mode-my-wordpress__footprint-event-meta";
7911 const parts = [formatLongDate(ev.date)];
7912 if (ev.status && ev.status !== "publish" && ev.status !== "approved") {
7913 parts.push(ev.status);
7914 }
7915 meta.textContent = parts.join(" · ");
7916 body.appendChild(meta);
7917 li.appendChild(body);
7918 list.appendChild(li);
7919 }
7920 section.appendChild(list);
7921 return section;
7922 }
7923 function buildFootprintFooter(payload, userId, userName) {
7924 const footer = document.createElement("footer");
7925 footer.className = "desktop-mode-my-wordpress__footprint-section desktop-mode-my-wordpress__footprint-footer";
7926 const archiveBtn = document.createElement("wpd-button");
7927 archiveBtn.setAttribute("variant", "ghost");
7928 archiveBtn.textContent = __("View author archive", "desktop-mode");
7929 archiveBtn.addEventListener("click", () => {
7930 if (payload.profile.link) {
7931 window.open(payload.profile.link, "_blank", "noopener,noreferrer");
7932 }
7933 });
7934 if (!payload.profile.link) {
7935 archiveBtn.setAttribute("disabled", "");
7936 }
7937 footer.appendChild(archiveBtn);
7938 const editBtn = document.createElement("wpd-button");
7939 editBtn.setAttribute("variant", "primary");
7940 editBtn.textContent = __("Show profile", "desktop-mode");
7941 editBtn.addEventListener("click", () => {
7942 openUserEditWindow(userId);
7943 });
7944 footer.appendChild(editBtn);
7945 return footer;
7946 }
7947 function formatShortDate(iso) {
7948 if (!iso) {
7949 return "";
7950 }
7951 try {
7952 return new Date(iso).toLocaleDateString(void 0, {
7953 month: "short",
7954 day: "numeric"
7955 });
7956 } catch {
7957 return iso;
7958 }
7959 }
7960 function formatLongDate(iso) {
7961 if (!iso) {
7962 return "";
7963 }
7964 try {
7965 return new Date(iso).toLocaleDateString(void 0, {
7966 year: "numeric",
7967 month: "short",
7968 day: "numeric"
7969 });
7970 } catch {
7971 return iso;
7972 }
7973 }
7974 function sanitizeClass(raw) {
7975 return (raw || "").replace(/[^a-zA-Z0-9_-]/g, "");
7976 }
7977 function extractContentMediaIds(html2) {
7978 if (!html2 || typeof html2 !== "string") {
7979 return [];
7980 }
7981 const ids = [];
7982 const seen = /* @__PURE__ */ new Set();
7983 const push = (raw) => {
7984 const id = parseInt(raw, 10);
7985 if (Number.isFinite(id) && id > 0 && !seen.has(id)) {
7986 seen.add(id);
7987 ids.push(id);
7988 }
7989 };
7990 const wpImage = /\bwp-image-(\d+)\b/g;
7991 let m;
7992 while ((m = wpImage.exec(html2)) !== null) {
7993 push(m[1]);
7994 }
7995 const captionShort = /\[caption[^\]]*id="attachment_(\d+)"/g;
7996 while ((m = captionShort.exec(html2)) !== null) {
7997 push(m[1]);
7998 }
7999 return ids;
8000 }
8001 function createTileSelector() {
8002 let selected = null;
8003 return (tile) => {
8004 if (selected === tile) {
8005 return;
8006 }
8007 if (selected) {
8008 selected.classList.remove(
8009 "desktop-mode-file-tile--selected"
8010 );
8011 }
8012 tile.classList.add("desktop-mode-file-tile--selected");
8013 selected = tile;
8014 };
8015 }
8016 const TILE_W = 108;
8017 const TILE_H = 112;
8018 const TILE_PAD = 16;
8019 function createTileLayout(host, scope) {
8020 const positions = loadPositions(scope);
8021 const entries = [];
8022 const occupied = /* @__PURE__ */ new Set();
8023 host.classList.add("desktop-mode-my-wordpress__canvas--positioned");
8024 const cellOf = (x, y) => ({
8025 col: Math.max(0, Math.round((x - TILE_PAD) / TILE_W)),
8026 row: Math.max(0, Math.round((y - TILE_PAD) / TILE_H))
8027 });
8028 const occupyAt = (x, y) => {
8029 const { col, row } = cellOf(x, y);
8030 occupied.add(`${col},${row}`);
8031 };
8032 const releaseAt = (x, y) => {
8033 const { col, row } = cellOf(x, y);
8034 occupied.delete(`${col},${row}`);
8035 };
8036 const recomputeHostHeight = () => {
8037 let maxBottom = 0;
8038 for (const child of Array.from(host.children)) {
8039 if (!(child instanceof HTMLElement)) {
8040 continue;
8041 }
8042 if (!child.classList.contains("desktop-mode-file-tile")) {
8043 continue;
8044 }
8045 const top = parseFloat(child.style.top || "0");
8046 maxBottom = Math.max(maxBottom, top + TILE_H);
8047 }
8048 host.style.minHeight = `${Math.max(0, maxBottom + TILE_PAD)}px`;
8049 };
8050 const nextFreeCell = (cols) => {
8051 for (let n = 0; ; n += 1) {
8052 const col = n % cols;
8053 const row = Math.floor(n / cols);
8054 if (!occupied.has(`${col},${row}`)) {
8055 return { col, row };
8056 }
8057 }
8058 };
8059 const place = (tile, key, sortable) => {
8060 const saved = positions[key];
8061 const width = host.clientWidth > 0 ? host.clientWidth : TILE_PAD + 5 * TILE_W;
8062 const cols = Math.max(
8063 1,
8064 Math.floor((width - TILE_PAD) / TILE_W)
8065 );
8066 const fits = saved && saved.x + TILE_W <= width;
8067 const entry = {
8068 key,
8069 tile,
8070 sortable,
8071 userPlaced: !!fits
8072 };
8073 entries.push(entry);
8074 let x;
8075 let y;
8076 if (fits && saved) {
8077 x = saved.x;
8078 y = saved.y;
8079 } else {
8080 if (saved && !fits) {
8081 delete positions[key];
8082 savePositions(scope, positions);
8083 }
8084 const cell = nextFreeCell(cols);
8085 x = TILE_PAD + cell.col * TILE_W;
8086 y = TILE_PAD + cell.row * TILE_H;
8087 }
8088 occupyAt(x, y);
8089 applyTilePosition(tile, x, y);
8090 recomputeHostHeight();
8091 };
8092 const commit = (tile, key, x, y) => {
8093 const oldX = parseFloat(tile.style.left || "0");
8094 const oldY = parseFloat(tile.style.top || "0");
8095 releaseAt(oldX, oldY);
8096 applyTilePosition(tile, x, y);
8097 occupyAt(x, y);
8098 positions[key] = { x, y };
8099 savePositions(scope, positions);
8100 const entry = entries.find((e) => e.key === key);
8101 if (entry) {
8102 entry.userPlaced = true;
8103 }
8104 recomputeHostHeight();
8105 };
8106 const reflow = () => {
8107 const width = host.clientWidth > 0 ? host.clientWidth : TILE_PAD + 5 * TILE_W;
8108 const cols = Math.max(
8109 1,
8110 Math.floor((width - TILE_PAD) / TILE_W)
8111 );
8112 const overflowing = entries.some((entry) => {
8113 const left = parseFloat(entry.tile.style.left || "0");
8114 return left + TILE_W > width;
8115 });
8116 if (overflowing) {
8117 for (const k of Object.keys(positions)) {
8118 delete positions[k];
8119 }
8120 savePositions(scope, positions);
8121 for (const entry of entries) {
8122 entry.userPlaced = false;
8123 }
8124 }
8125 occupied.clear();
8126 for (const entry of entries) {
8127 if (!entry.userPlaced) {
8128 continue;
8129 }
8130 const left = parseFloat(entry.tile.style.left || "0");
8131 const top = parseFloat(entry.tile.style.top || "0");
8132 occupyAt(left, top);
8133 }
8134 let autoCount = 0;
8135 for (const entry of entries) {
8136 if (entry.userPlaced) {
8137 continue;
8138 }
8139 const cell = nextFreeCell(cols);
8140 const x = TILE_PAD + cell.col * TILE_W;
8141 const y = TILE_PAD + cell.row * TILE_H;
8142 applyTilePosition(entry.tile, x, y);
8143 occupyAt(x, y);
8144 autoCount += 1;
8145 }
8146 recomputeHostHeight();
8147 doAction("desktop-mode.icon-canvas.reflow", {
8148 scope,
8149 cols,
8150 autoCount,
8151 overflowing
8152 });
8153 };
8154 const sort = (mode) => {
8155 const sorted = entries.slice().sort((a, b) => {
8156 switch (mode) {
8157 case "name-asc":
8158 return a.sortable.name.localeCompare(b.sortable.name);
8159 case "name-desc":
8160 return b.sortable.name.localeCompare(a.sortable.name);
8161 case "date-asc":
8162 return Date.parse(a.sortable.date) - Date.parse(b.sortable.date);
8163 case "date-desc":
8164 return Date.parse(b.sortable.date) - Date.parse(a.sortable.date);
8165 default:
8166 return 0;
8167 }
8168 });
8169 const width = host.clientWidth > 0 ? host.clientWidth : TILE_PAD + 5 * TILE_W;
8170 const cols = Math.max(
8171 1,
8172 Math.floor((width - TILE_PAD) / TILE_W)
8173 );
8174 for (const k of Object.keys(positions)) {
8175 delete positions[k];
8176 }
8177 occupied.clear();
8178 sorted.forEach((entry, idx) => {
8179 const col = idx % cols;
8180 const row = Math.floor(idx / cols);
8181 const x = TILE_PAD + col * TILE_W;
8182 const y = TILE_PAD + row * TILE_H;
8183 applyTilePosition(entry.tile, x, y);
8184 occupyAt(x, y);
8185 positions[entry.key] = { x, y };
8186 entry.userPlaced = true;
8187 });
8188 savePositions(scope, positions);
8189 for (const entry of sorted) {
8190 host.appendChild(entry.tile);
8191 }
8192 recomputeHostHeight();
8193 };
8194 let lastWidth = host.clientWidth;
8195 let resizeObserver = null;
8196 if (typeof ResizeObserver !== "undefined") {
8197 resizeObserver = new ResizeObserver(() => {
8198 const w = host.clientWidth;
8199 if (w === lastWidth) {
8200 return;
8201 }
8202 lastWidth = w;
8203 reflow();
8204 });
8205 resizeObserver.observe(host);
8206 }
8207 const peekNextCells = (count) => {
8208 const width = host.clientWidth > 0 ? host.clientWidth : TILE_PAD + 5 * TILE_W;
8209 const cols = Math.max(
8210 1,
8211 Math.floor((width - TILE_PAD) / TILE_W)
8212 );
8213 const taken = new Set(occupied);
8214 const out = [];
8215 for (let i = 0; i < count; i += 1) {
8216 for (let n = 0; ; n += 1) {
8217 const col = n % cols;
8218 const row = Math.floor(n / cols);
8219 const key = `${col},${row}`;
8220 if (taken.has(key)) {
8221 continue;
8222 }
8223 taken.add(key);
8224 out.push({
8225 x: TILE_PAD + col * TILE_W,
8226 y: TILE_PAD + row * TILE_H
8227 });
8228 break;
8229 }
8230 }
8231 return out;
8232 };
8233 const clear = () => {
8234 entries.length = 0;
8235 occupied.clear();
8236 host.style.minHeight = "";
8237 };
8238 return {
8239 host,
8240 scope,
8241 place,
8242 commit,
8243 sort,
8244 reflow,
8245 peekNextCells,
8246 clear,
8247 dispose: () => {
8248 resizeObserver?.disconnect();
8249 resizeObserver = null;
8250 }
8251 };
8252 }
8253 function applyTilePosition(tile, x, y) {
8254 tile.style.left = `${Math.round(x)}px`;
8255 tile.style.top = `${Math.round(y)}px`;
8256 }
8257 function loadPositions(scope) {
8258 try {
8259 const raw = window.localStorage.getItem(storageKey(scope));
8260 if (!raw) {
8261 return {};
8262 }
8263 const parsed = JSON.parse(raw);
8264 return parsed && typeof parsed === "object" ? parsed : {};
8265 } catch {
8266 return {};
8267 }
8268 }
8269 function savePositions(scope, positions) {
8270 try {
8271 window.localStorage.setItem(
8272 storageKey(scope),
8273 JSON.stringify(positions)
8274 );
8275 } catch {
8276 }
8277 }
8278 function storageKey(scope) {
8279 return `desktop-mode-my-wordpress:positions:${scope}`;
8280 }
8281 let activeState = null;
8282 const liveStates = /* @__PURE__ */ new Map();
8283 let pendingRoute = null;
8284 let rejectIdCounter = 0;
8285 function renderInto(body) {
8286 const root = body.querySelector(ROOT_SEL);
8287 if (!root) {
8288 return void 0;
8289 }
8290 const breadcrumbsHost = root.querySelector(BREADCRUMBS_SEL);
8291 const bodyHost = root.querySelector(BODY_SEL);
8292 const statusHost = root.querySelector(STATUS_SEL);
8293 if (!breadcrumbsHost || !bodyHost || !statusHost) {
8294 return void 0;
8295 }
8296 const state = {
8297 route: { kind: "root" },
8298 body: bodyHost,
8299 root,
8300 breadcrumbs: breadcrumbsHost,
8301 statusBar: statusHost,
8302 teardown: [],
8303 history: []
8304 };
8305 activeState = state;
8306 liveStates.set(bodyHost, state);
8307 const windowTeardowns = [];
8308 const dragManager = getDragManager();
8309 if (dragManager) {
8310 rejectIdCounter += 1;
8311 const deregister = dragManager.registerDropTarget({
8312 id: `${WINDOW_ID}-reject-${rejectIdCounter}`,
8313 element: body,
8314 accept: () => false,
8315 onDrop: () => {
8316 }
8317 });
8318 windowTeardowns.push(deregister);
8319 }
8320 windowTeardowns.push(() => closeAnyTileMenu());
8321 const initialRoute = pendingRoute ?? { kind: "root" };
8322 pendingRoute = null;
8323 navigate(state, initialRoute);
8324 return () => {
8325 clearTeardown(state);
8326 for (const fn of windowTeardowns) {
8327 try {
8328 fn();
8329 } catch {
8330 }
8331 }
8332 windowTeardowns.length = 0;
8333 liveStates.delete(bodyHost);
8334 if (activeState === state) {
8335 const next = liveStates.size > 0 ? Array.from(liveStates.values()).pop() : null;
8336 activeState = next;
8337 }
8338 };
8339 }
8340 const callback = (body) => {
8341 try {
8342 return renderInto(body);
8343 } catch (err) {
8344 console.error("[my-wordpress] render failed:", err);
8345 return void 0;
8346 }
8347 };
8348 window.desktopModeNativeWindows = window.desktopModeNativeWindows || {};
8349 window.desktopModeNativeWindows[WINDOW_ID] = callback;
8350 registerEntityKind("post", (host, entity) => {
8351 renderEntityList(asRenderState(host), entity);
8352 });
8353 registerEntityKind("user", (host, entity) => {
8354 renderUserEntityList(asRenderState(host), entity);
8355 });
8356 registerEntityKind("media", renderMediaList);
8357 function asRenderState(host) {
8358 const found = liveStates.get(host.body);
8359 if (found) {
8360 return found;
8361 }
8362 if (activeState && host.body === activeState.body) {
8363 return activeState;
8364 }
8365 throw new Error(
8366 "[my-wordpress] asRenderState: host body does not match any live render state."
8367 );
8368 }
8369 function openDetail(args) {
8370 const route = {
8371 kind: "detail",
8372 entityId: args.entityId,
8373 postId: args.postId,
8374 postTitle: args.postTitle
8375 };
8376 if (activeState) {
8377 navigate(activeState, route);
8378 return;
8379 }
8380 pendingRoute = route;
8381 const desktop = window.wp?.desktop;
8382 desktop?.openWindow?.(WINDOW_ID, { source: "my-wordpress/open-detail" });
8383 }
8384 function openMedia(args) {
8385 const route = {
8386 kind: "media-detail",
8387 entityId: "media",
8388 mediaId: args.mediaId,
8389 mediaTitle: args.mediaTitle ?? `#${args.mediaId}`
8390 };
8391 if (activeState) {
8392 navigate(activeState, route);
8393 return;
8394 }
8395 pendingRoute = route;
8396 const desktop = window.wp?.desktop;
8397 desktop?.openWindow?.(WINDOW_ID, { source: "my-wordpress/open-media" });
8398 }
8399 const desktopGlobal = window.wp?.desktop;
8400 if (desktopGlobal) {
8401 const pending = desktopGlobal.myWordpress?.__pendingKinds;
8402 if (Array.isArray(pending)) {
8403 for (const entry of pending) {
8404 try {
8405 entry.slot.unregister = registerEntityKind(
8406 entry.kind,
8407 entry.renderer
8408 );
8409 } catch (err) {
8410 console.error(
8411 `[my-wordpress] queued registerEntityKind('${entry.kind}') failed:`,
8412 err
8413 );
8414 }
8415 }
8416 pending.length = 0;
8417 }
8418 desktopGlobal.myWordpress = {
8419 openDetail,
8420 openMedia,
8421 registerEntityKind
8422 };
8423 }
8424 })();
8425