PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.1
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.1, at assets/js/my-wordpress.js

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