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

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

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