PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.6
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 / plugins-window.js

plugins-window.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.6, at assets/js/plugins-window.js

10,199 lines 353.8 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 NONCE_HEADER = "X-WP-Nonce";
4 function injectRestNonce(input, init) {
5 const nonce = readRestNonce();
6 if (!nonce) {
7 return init;
8 }
9 const url = resolveUrl(input);
10 if (!url || !isSameOriginRestUrl(url)) {
11 return init;
12 }
13 const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
14 const headers = new Headers(baseHeaders ?? {});
15 if (headers.has(NONCE_HEADER)) {
16 return init;
17 }
18 headers.set(NONCE_HEADER, nonce);
19 return { ...init ?? {}, headers };
20 }
21 function readRestNonce() {
22 if (typeof window === "undefined") {
23 return void 0;
24 }
25 const cfg = window.desktopModeConfig;
26 const value = cfg?.restNonce;
27 return typeof value === "string" && value.length > 0 ? value : void 0;
28 }
29 function resolveUrl(input) {
30 try {
31 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
32 if (typeof input === "string") {
33 return new URL(input, base);
34 }
35 if (input instanceof URL) {
36 return input;
37 }
38 if (typeof Request !== "undefined" && input instanceof Request) {
39 return new URL(input.url, base);
40 }
41 return null;
42 } catch {
43 return null;
44 }
45 }
46 function isSameOriginRestUrl(url) {
47 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
48 return false;
49 }
50 if (url.pathname.includes("/wp-json/")) {
51 return true;
52 }
53 if (url.searchParams.has("rest_route")) {
54 return true;
55 }
56 return false;
57 }
58 function trackedFetch(input, init, opts = {}) {
59 const fn = window.wp?.desktop?.fetch;
60 if (typeof fn === "function") {
61 return fn(input, init, opts);
62 }
63 const finalInit = injectRestNonce(input, init);
64 return fetch(input, finalInit);
65 }
66 const TEXT_DOMAIN = "desktop-mode";
67 function i18n() {
68 return window.wp?.i18n;
69 }
70 function __(text, domain = TEXT_DOMAIN) {
71 return i18n()?.__(text, domain) ?? text;
72 }
73 function sprintf(format, ...args) {
74 const impl = i18n()?.sprintf;
75 if (impl) {
76 return impl(format, ...args);
77 }
78 let i = 0;
79 return format.replace(/%(?:(\d+)\$)?[sd]/g, (_match, pos) => {
80 const idx = pos ? Number.parseInt(pos, 10) - 1 : i++;
81 return String(args[idx] ?? "");
82 });
83 }
84 function html(strings, ...values) {
85 return { __wpdHtml: true, strings, values };
86 }
87 function isTemplateResult$1(v) {
88 return !!v && v.__wpdHtml === true;
89 }
90 const MARKER_PREFIX = "$$wpd$$";
91 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
92 function joinWithMarkers(strings) {
93 let out = strings[0];
94 for (let i = 1; i < strings.length; i++) {
95 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
96 }
97 return out;
98 }
99 const compiledCache = /* @__PURE__ */ new WeakMap();
100 function compile(strings) {
101 const cached = compiledCache.get(strings);
102 if (cached) {
103 return cached;
104 }
105 const template = document.createElement("template");
106 template.innerHTML = joinWithMarkers(strings);
107 const recipes = [];
108 const walk = (node, path) => {
109 if (node.nodeType === Node.ELEMENT_NODE) {
110 const el = node;
111 for (const attr of Array.from(el.attributes)) {
112 const rawName = attr.name;
113 const rawValue = attr.value;
114 const prefix = rawName[0];
115 if (MARKER_RE.test(rawValue)) {
116 MARKER_RE.lastIndex = 0;
117 if (prefix === "@") {
118 const match = MARKER_RE.exec(rawValue);
119 MARKER_RE.lastIndex = 0;
120 recipes.push({
121 path,
122 kind: "event",
123 name: rawName.slice(1),
124 valueIndex: match ? Number(match[1]) : 0
125 });
126 el.removeAttribute(rawName);
127 } else if (prefix === ".") {
128 const match = MARKER_RE.exec(rawValue);
129 MARKER_RE.lastIndex = 0;
130 recipes.push({
131 path,
132 kind: "prop",
133 name: rawName.slice(1),
134 valueIndex: match ? Number(match[1]) : 0
135 });
136 el.removeAttribute(rawName);
137 } else if (prefix === "?") {
138 const match = MARKER_RE.exec(rawValue);
139 MARKER_RE.lastIndex = 0;
140 recipes.push({
141 path,
142 kind: "bool",
143 name: rawName.slice(1),
144 valueIndex: match ? Number(match[1]) : 0
145 });
146 el.removeAttribute(rawName);
147 } else {
148 const fragments = [];
149 const indices = [];
150 let lastEnd = 0;
151 let m;
152 MARKER_RE.lastIndex = 0;
153 while ((m = MARKER_RE.exec(rawValue)) !== null) {
154 fragments.push(rawValue.slice(lastEnd, m.index));
155 indices.push(Number(m[1]));
156 lastEnd = m.index + m[0].length;
157 }
158 fragments.push(rawValue.slice(lastEnd));
159 recipes.push({
160 path,
161 kind: "attr",
162 name: rawName,
163 template: fragments,
164 valueIndices: indices
165 });
166 el.setAttribute(rawName, "");
167 }
168 }
169 }
170 }
171 const children = Array.from(node.childNodes);
172 let shift = 0;
173 for (let i = 0; i < children.length; i++) {
174 const child = children[i];
175 const liveIndex = i + shift;
176 if (child.nodeType === Node.TEXT_NODE) {
177 const text = child.textContent || "";
178 if (!MARKER_RE.test(text)) {
179 MARKER_RE.lastIndex = 0;
180 continue;
181 }
182 MARKER_RE.lastIndex = 0;
183 const parent = child.parentNode;
184 let lastEnd = 0;
185 let m;
186 const newNodes = [];
187 const newRecipes = [];
188 MARKER_RE.lastIndex = 0;
189 while ((m = MARKER_RE.exec(text)) !== null) {
190 if (m.index > lastEnd) {
191 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
192 }
193 const placeholder = document.createTextNode("");
194 newNodes.push(placeholder);
195 newRecipes.push({
196 path: [...path, liveIndex + newNodes.length - 1],
197 kind: "node",
198 valueIndex: Number(m[1])
199 });
200 lastEnd = m.index + m[0].length;
201 }
202 if (lastEnd < text.length) {
203 newNodes.push(document.createTextNode(text.slice(lastEnd)));
204 }
205 for (const nn of newNodes) {
206 parent.insertBefore(nn, child);
207 }
208 parent.removeChild(child);
209 shift += newNodes.length - 1;
210 recipes.push(...newRecipes);
211 } else {
212 walk(child, [...path, liveIndex]);
213 }
214 }
215 };
216 walk(template.content, []);
217 const buildParts = (fragment) => {
218 const out = [];
219 for (const r of recipes) {
220 let node = fragment;
221 for (const idx of r.path) {
222 node = node.childNodes[idx];
223 }
224 if (r.kind === "node") {
225 out.push({
226 kind: "node",
227 valueIndex: r.valueIndex,
228 child: {
229 anchor: node,
230 state: null
231 }
232 });
233 } else if (r.kind === "attr") {
234 out.push({
235 kind: "attr",
236 element: node,
237 name: r.name,
238 template: r.template,
239 valueIndices: r.valueIndices
240 });
241 } else if (r.kind === "event") {
242 out.push({
243 kind: "event",
244 valueIndex: r.valueIndex,
245 element: node,
246 name: r.name
247 });
248 } else if (r.kind === "prop") {
249 out.push({
250 kind: "prop",
251 valueIndex: r.valueIndex,
252 element: node,
253 name: r.name
254 });
255 } else if (r.kind === "bool") {
256 out.push({
257 kind: "bool",
258 valueIndex: r.valueIndex,
259 element: node,
260 name: r.name
261 });
262 }
263 }
264 return out;
265 };
266 const entry = { template, buildParts };
267 compiledCache.set(strings, entry);
268 return entry;
269 }
270 const mountState = /* @__PURE__ */ new WeakMap();
271 function mountIntact(state, container) {
272 for (const node of state.nodes) {
273 if (node.parentNode !== container) {
274 return false;
275 }
276 }
277 return true;
278 }
279 function render(result, container) {
280 const existing = mountState.get(container);
281 if (existing && existing.strings === result.strings && mountIntact(existing, container)) {
282 applyValues(existing.parts, result.values);
283 return;
284 }
285 const compiled = compile(result.strings);
286 const fragment = compiled.template.content.cloneNode(true);
287 const parts = compiled.buildParts(fragment);
288 const nodes = Array.from(fragment.childNodes);
289 while (container.firstChild) {
290 container.removeChild(container.firstChild);
291 }
292 container.appendChild(fragment);
293 applyValues(parts, result.values);
294 mountState.set(container, { strings: result.strings, parts, nodes });
295 }
296 function applyValues(parts, values) {
297 for (const part of parts) {
298 if (part.kind === "node") {
299 updateChildPart(part.child, values[part.valueIndex]);
300 } else if (part.kind === "attr") {
301 let composed = part.template[0];
302 for (let i = 0; i < part.valueIndices.length; i++) {
303 composed += formatText(values[part.valueIndices[i]]);
304 composed += part.template[i + 1];
305 }
306 if (composed !== part.last) {
307 part.last = composed;
308 if (composed === "") {
309 part.element.removeAttribute(part.name);
310 } else {
311 part.element.setAttribute(part.name, composed);
312 }
313 }
314 } else if (part.kind === "event") {
315 const next = values[part.valueIndex];
316 if (next !== part.current) {
317 if (part.current) {
318 part.element.removeEventListener(part.name, part.current);
319 }
320 if (next) {
321 part.element.addEventListener(part.name, next);
322 }
323 part.current = next;
324 }
325 } else if (part.kind === "prop") {
326 const next = values[part.valueIndex];
327 if (next !== part.last) {
328 part.last = next;
329 part.element[part.name] = next;
330 }
331 } else if (part.kind === "bool") {
332 const next = !!values[part.valueIndex];
333 if (next !== part.last) {
334 part.last = next;
335 if (next) {
336 part.element.setAttribute(part.name, "");
337 } else {
338 part.element.removeAttribute(part.name);
339 }
340 }
341 }
342 }
343 }
344 function updateChildPart(child, value) {
345 if (value === null || value === void 0 || value === false) {
346 if (child.state) {
347 disposeChildState(child.state);
348 child.state = null;
349 }
350 return;
351 }
352 if (Array.isArray(value)) {
353 updateArrayChild(child, value);
354 return;
355 }
356 if (isTemplateResult$1(value)) {
357 updateTemplateChild(child, value);
358 return;
359 }
360 if (value instanceof Node) {
361 updateNodeChild(child, value);
362 return;
363 }
364 updateTextChild(child, formatText(value));
365 }
366 function updateNodeChild(child, node) {
367 const old = child.state;
368 if (old?.shape === "node" && old.node === node) {
369 return;
370 }
371 if (old) {
372 disposeChildState(old);
373 }
374 insertBeforeAnchor(child, [node]);
375 child.state = { shape: "node", node };
376 }
377 function updateTextChild(child, text) {
378 const old = child.state;
379 if (old?.shape === "text") {
380 if (old.text !== text) {
381 old.node.textContent = text;
382 old.text = text;
383 }
384 return;
385 }
386 if (old) {
387 disposeChildState(old);
388 }
389 const node = document.createTextNode(text);
390 insertBeforeAnchor(child, [node]);
391 child.state = { shape: "text", node, text };
392 }
393 function updateTemplateChild(child, result) {
394 const old = child.state;
395 if (old?.shape === "template" && old.strings === result.strings) {
396 applyValues(old.parts, result.values);
397 return;
398 }
399 if (old) {
400 disposeChildState(old);
401 }
402 const compiled = compile(result.strings);
403 const fragment = compiled.template.content.cloneNode(true);
404 const parts = compiled.buildParts(fragment);
405 const topNodes = Array.from(fragment.childNodes);
406 insertBeforeAnchor(child, [fragment]);
407 applyValues(parts, result.values);
408 child.state = {
409 shape: "template",
410 strings: result.strings,
411 parts,
412 nodes: topNodes
413 };
414 }
415 function updateArrayChild(child, arr) {
416 const old = child.state;
417 if (old?.shape === "array" && old.entries.length === arr.length) {
418 for (let i = 0; i < arr.length; i++) {
419 updateChildPart(old.entries[i], arr[i]);
420 }
421 return;
422 }
423 if (old) {
424 disposeChildState(old);
425 }
426 const entries = [];
427 for (const v of arr) {
428 const entryAnchor = document.createTextNode("");
429 insertBeforeAnchor(child, [entryAnchor]);
430 const entry = { anchor: entryAnchor, state: null };
431 updateChildPart(entry, v);
432 entries.push(entry);
433 }
434 child.state = { shape: "array", entries };
435 }
436 function insertBeforeAnchor(child, nodes) {
437 const parent = child.anchor.parentNode;
438 if (!parent) {
439 return;
440 }
441 for (const node of nodes) {
442 parent.insertBefore(node, child.anchor);
443 }
444 }
445 function disposeChildState(state) {
446 if (state.shape === "text") {
447 state.node.remove();
448 return;
449 }
450 if (state.shape === "template") {
451 for (const node of state.nodes) {
452 if (node.parentNode) {
453 node.parentNode.removeChild(node);
454 }
455 }
456 return;
457 }
458 if (state.shape === "node") {
459 if (state.node.parentNode) {
460 state.node.parentNode.removeChild(state.node);
461 }
462 return;
463 }
464 for (const entry of state.entries) {
465 if (entry.state) {
466 disposeChildState(entry.state);
467 }
468 entry.anchor.remove();
469 }
470 }
471 function formatText(v) {
472 if (v === null || v === void 0 || v === false) {
473 return "";
474 }
475 return String(v);
476 }
477 const _Component = class _Component extends HTMLElement {
478 constructor() {
479 super();
480 this._renderScheduled = false;
481 this._propValues = {};
482 const ctor = this.constructor;
483 if (ctor.shadow) {
484 this.attachShadow({ mode: "open" });
485 this._renderRoot = this.shadowRoot;
486 } else {
487 this._renderRoot = this;
488 }
489 this._installPropAccessors();
490 }
491 static get observedAttributes() {
492 return this.props.map(kebab);
493 }
494 connectedCallback() {
495 this._adoptStyles();
496 this.requestUpdate();
497 }
498 attributeChangedCallback(name, oldValue, newValue) {
499 if (oldValue === newValue) {
500 return;
501 }
502 const prop = camel(name);
503 this._propValues[prop] = newValue;
504 this.requestUpdate();
505 }
506 /**
507 * Declarative class-name setter. Assign an array (or a
508 * space-separated string) and the host's `class` attribute is
509 * rewritten to match. Intended for programmatic styling — when
510 * a plugin has enqueued its own stylesheet and wants to apply
511 * one of those classes to a shell component:
512 *
513 * ```js
514 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
515 * // → <wpd-select class="my-plugin-brand is-active">
516 * ```
517 *
518 * The plain HTML `class="…"` attribute works just the same and
519 * is always preferred when writing markup by hand — this setter
520 * exists for the JS-API case where the caller has an array of
521 * conditional classes in hand.
522 *
523 * Getter returns the current `classList` as a plain array for
524 * symmetric read/write.
525 *
526 * @since 0.5.0
527 */
528 get classNames() {
529 return Array.from(this.classList);
530 }
531 set classNames(next) {
532 if (next === null || next === void 0) {
533 this.removeAttribute("class");
534 return;
535 }
536 const list = Array.isArray(next) ? next : String(next).split(/\s+/);
537 const cleaned = list.map((s) => String(s).trim()).filter((s) => s !== "");
538 this.className = cleaned.join(" ");
539 }
540 /**
541 * Request a re-render explicitly. Components rarely need this —
542 * declare state via props + attribute observers and the render
543 * loop picks up changes automatically.
544 */
545 requestUpdate() {
546 this._scheduleRender();
547 }
548 /**
549 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
550 * by default (matches typical WC UX — events cross shadow
551 * boundaries, parents can listen without knowing about internal
552 * structure).
553 */
554 emit(name, detail) {
555 return this.dispatchEvent(
556 new CustomEvent(name, {
557 detail,
558 bubbles: true,
559 composed: true
560 })
561 );
562 }
563 // ------------------------------------------------------------------
564 // Internals
565 // ------------------------------------------------------------------
566 /**
567 * Wire every `static props` entry to a matched property getter +
568 * setter on the element. Setting the property reflects into the
569 * attribute (so downstream observers + CSS selectors see it);
570 * reading the property falls back to the attribute.
571 */
572 _installPropAccessors() {
573 const ctor = this.constructor;
574 for (const prop of ctor.props) {
575 if (Object.getOwnPropertyDescriptor(this, prop)) {
576 continue;
577 }
578 const attr = kebab(prop);
579 Object.defineProperty(this, prop, {
580 get: () => {
581 if (prop in this._propValues) {
582 return this._propValues[prop];
583 }
584 return this.getAttribute(attr);
585 },
586 set: (value) => {
587 let str;
588 if (value === null || value === void 0 || value === false) {
589 str = null;
590 } else if (value === true) {
591 str = "";
592 } else {
593 str = String(value);
594 }
595 this._propValues[prop] = str;
596 if (str === null) {
597 this.removeAttribute(attr);
598 } else {
599 this.setAttribute(attr, str);
600 }
601 this.requestUpdate();
602 },
603 enumerable: true,
604 configurable: true
605 });
606 }
607 }
608 /**
609 * Schedule a render on the next microtask. Multiple property
610 * assignments in the same tick collapse into a single render.
611 */
612 _scheduleRender() {
613 if (this._renderScheduled || !this.isConnected) {
614 return;
615 }
616 this._renderScheduled = true;
617 queueMicrotask(() => {
618 this._renderScheduled = false;
619 if (!this.isConnected) {
620 return;
621 }
622 render(this.render(), this._renderRoot);
623 });
624 }
625 /**
626 * Mount adoptable stylesheets onto the shadow root (via
627 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
628 * tag per def). No-op if `static styles` is empty.
629 */
630 _adoptStyles() {
631 const ctor = this.constructor;
632 if (ctor.styles.length === 0) {
633 return;
634 }
635 if (ctor.shadow && this.shadowRoot) {
636 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
637 this.shadowRoot.adoptedStyleSheets = sheets;
638 if (sheets.length !== ctor.styles.length) {
639 for (const s of ctor.styles) {
640 if (!s.sheet) {
641 const tag = document.createElement("style");
642 tag.textContent = s.cssText;
643 this.shadowRoot.appendChild(tag);
644 }
645 }
646 }
647 } else {
648 this._adoptLightStyles(ctor);
649 }
650 }
651 _adoptLightStyles(ctor) {
652 if (_Component._lightStylesAdopted.has(ctor)) {
653 return;
654 }
655 _Component._lightStylesAdopted.add(ctor);
656 for (const s of ctor.styles) {
657 const tag = document.createElement("style");
658 tag.dataset.wpdUi = this.tagName.toLowerCase();
659 tag.textContent = s.cssText;
660 document.head.appendChild(tag);
661 }
662 }
663 };
664 _Component.props = [];
665 _Component.styles = [];
666 _Component.shadow = true;
667 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
668 let Component = _Component;
669 function defineComponent(tag, ctor) {
670 if (customElements.get(tag)) {
671 return;
672 }
673 customElements.define(tag, ctor);
674 }
675 function kebab(s) {
676 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
677 }
678 function camel(s) {
679 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
680 }
681 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
682 try {
683 const s = new CSSStyleSheet();
684 return typeof s.replaceSync === "function";
685 } catch {
686 return false;
687 }
688 })();
689 function css(strings, ...values) {
690 let text = strings[0];
691 for (let i = 1; i < strings.length; i++) {
692 const v = values[i - 1];
693 if (typeof v === "string" || typeof v === "number") {
694 text += String(v);
695 } else if (v && v.__wpdCss) {
696 text += v.cssText;
697 } else {
698 throw new TypeError(
699 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
700 );
701 }
702 text += strings[i];
703 }
704 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
705 const sheet = new CSSStyleSheet();
706 sheet.replaceSync(text);
707 return { __wpdCss: true, sheet, cssText: text };
708 }
709 return { __wpdCss: true, sheet: null, cssText: text };
710 }
711 function computeAutoId(element) {
712 const parts = [];
713 const tabs = [];
714 let windowId = null;
715 let node = element.parentElement;
716 while (node) {
717 if (node === document.body || node === document.documentElement) {
718 break;
719 }
720 const id = node.id || "";
721 if (id.startsWith("wp-window-")) {
722 windowId = id.slice("wp-window-".length);
723 break;
724 }
725 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
726 const forValue = node.getAttribute("for");
727 if (forValue) {
728 tabs.unshift(forValue);
729 }
730 }
731 node = node.parentElement;
732 }
733 if (windowId) {
734 parts.push(slugify(windowId));
735 }
736 for (const tab of tabs) {
737 parts.push("tab-" + slugify(tab));
738 }
739 const label = element.getAttribute("label");
740 if (label) {
741 parts.push(slugify(label));
742 }
743 if (parts.length === 0) {
744 return "wpd-unnamed";
745 }
746 return "wpd-" + parts.filter((p) => p !== "").join("-");
747 }
748 function slugify(s) {
749 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
750 }
751 function ensureAutoId(element) {
752 if (element.id) {
753 return element.id;
754 }
755 const id = computeAutoId(element);
756 element.id = id;
757 return id;
758 }
759 const styles$c = css`:host{display:block;--wpd-table-bg:var( --wpd-surface,#fff );--wpd-table-border:var( --wpd-border,rgba( 0,0,0,0.08 ) );--wpd-table-column-border:var( --wpd-border-strong,rgba( 0,0,0,0.14 ) );--wpd-table-header-bg:var( --wpd-surface-elevated,#f6f7f7 );--wpd-table-row-hover:rgba( 0,0,0,0.04 );--wpd-table-stripe:rgba( 0,0,0,0.03 );--wpd-table-cell-padding:8px 12px;--wpd-table-font-size:13px;--wpd-table-max-height:none;font-size:var( --wpd-table-font-size );color:inherit}:host( [ hidden ] ){display:none}.scroll{position:relative;overflow:auto;max-height:var( --wpd-table-max-height );border:1px solid var( --wpd-table-border );border-radius:4px;background:var( --wpd-table-bg )}table{width:100%;border-collapse:separate;border-spacing:0;background:var( --wpd-table-bg )}thead th{text-align:start;font-weight:600;background-color:var( --wpd-table-header-bg );padding:var( --wpd-table-cell-padding );border-bottom:1px solid var( --wpd-table-border );white-space:nowrap}tbody td{padding:var( --wpd-table-cell-padding );border-bottom:1px solid var( --wpd-table-border );background-color:var( --wpd-table-bg );vertical-align:middle}tbody tr:last-child td{border-bottom:0}:host( [ striped ] ) tbody tr:nth-child( odd ) td{background-image:linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) )}:host( [ hover ] ) tbody tr:hover td{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) )}:host( [ hover ] [ striped ] ) tbody tr:nth-child( odd ):hover td{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) ),linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) )}:host( [ compact ] ){--wpd-table-cell-padding:4px 8px;--wpd-table-font-size:12px}:host( [ bordered ] ) thead th,:host( [ bordered ] ) tbody td{border-inline-end:1px solid var( --wpd-table-column-border )}:host( [ bordered ] ) thead th:last-child,:host( [ bordered ] ) tbody td:last-child{border-inline-end:0}th.is-sticky,td.is-sticky{position:sticky;z-index:10}tbody td.is-sticky{background-color:var( --wpd-table-bg )}thead th.is-sticky{background-color:var( --wpd-table-header-bg );z-index:30}:host( [ sticky-header ] ) thead th{position:sticky;top:0;z-index:20}:host( [ sticky-header ] ) thead tr.filter-row th{top:var( --wpd-table-header-height,33px );z-index:20}:host( [ sticky-header ] ) thead th.is-sticky{z-index:40}:host( [ sticky-header ] ) thead tr.filter-row th.is-sticky{z-index:40}th.is-sticky-edge,td.is-sticky-edge{border-inline-end:var( --wpd-table-sticky-edge,2px solid var( --wpd-table-border ) )}.align-center{text-align:center}.align-end{text-align:end}.filter-row th{padding:4px 8px;background-color:var( --wpd-table-header-bg );border-bottom:1px solid var( --wpd-table-border );font-weight:400}.filter-input,.filter-select{width:100%;min-width:60px;box-sizing:border-box;padding:4px 6px;font:inherit;color:inherit;background-color:var( --wpd-table-bg );border:1px solid var( --wpd-table-border );border-radius:3px}.filter-input:focus,.filter-select:focus{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-1px}.expander{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:0;background:transparent;color:inherit;cursor:pointer;border-radius:3px;font-size:11px;line-height:1}.expander:hover{background:rgba( 0,0,0,0.06 )}td.col-expander,th.col-expander{width:36px;min-width:36px;padding-left:0;padding-right:0;text-align:center}tr.subtable td{padding:0;background-color:var( --wpd-table-bg );background-image:linear-gradient( var( --wpd-table-stripe ),var( --wpd-table-stripe ) );border-bottom:1px solid var( --wpd-table-border )}tr.subtable .subtable-inner{padding:8px 12px 8px 32px}tr.empty td{padding:24px;text-align:center;color:var( --wpd-text-muted,rgba( 0,0,0,0.55 ) );font-style:italic}thead th.is-sortable{cursor:pointer;user-select:none}thead th.is-sortable:hover{background-image:linear-gradient( var( --wpd-table-row-hover ),var( --wpd-table-row-hover ) )}thead th.is-sortable:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-2px}.sort-indicator{font-size:10px;color:var( --wpd-text-muted,rgba( 0,0,0,0.55 ) );margin-inline-start:2px}thead th.sort-asc .sort-indicator,thead th.sort-desc .sort-indicator{color:var( --wp-admin-theme-color,#2271b1 )}td.col-select,th.col-select{width:40px;min-width:40px;padding-left:0;padding-right:0;text-align:center}.select-all-checkbox,.select-row-checkbox{cursor:pointer;margin:0}tbody tr.is-selected td{background-color:color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 10%,var( --wpd-table-bg ) );background-image:none}tbody tr.is-selected:hover td{background-color:color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 16%,var( --wpd-table-bg ) )}tbody tr.skeleton td{padding:var( --wpd-table-cell-padding )}.skeleton-bar{display:block;height:12px;border-radius:3px;background:linear-gradient( 90deg,var( --wpd-table-skeleton-color,rgba( 0,0,0,0.06 ) ) 0%,var( --wpd-table-skeleton-highlight,rgba( 0,0,0,0.14 ) ) 50%,var( --wpd-table-skeleton-color,rgba( 0,0,0,0.06 ) ) 100% );background-size:200% 100%;animation:wpd-table-skeleton-pulse 1.4s ease-in-out infinite}@keyframes wpd-table-skeleton-pulse{0%{background-position:200% 50%}100%{background-position:-200% 50%}}@media ( prefers-reduced-motion:reduce ){.skeleton-bar{animation:none}}`;
760 const EXPANDER_KEY = "__wpd_expander__";
761 const SELECT_KEY = "__wpd_select__";
762 const _WpdTable = class _WpdTable extends Component {
763 constructor() {
764 super(...arguments);
765 this._data = [];
766 this._columns = [];
767 this._filters = {};
768 this._expanded = /* @__PURE__ */ new Set();
769 this._subTable = null;
770 this._sort = null;
771 this._selection = /* @__PURE__ */ new Set();
772 this._getRowId = (_row, index) => index;
773 this._filterCache = /* @__PURE__ */ new Map();
774 this._paintScheduled = false;
775 this._stickyHeaderWarned = false;
776 this._stickyRaceWarned = false;
777 this._resizeObserver = null;
778 this._stickyMicroScheduled = false;
779 this._stickyRafHandle = null;
780 this._loadingDesyncWarned = false;
781 this._lastStickyIndex = -1;
782 }
783 // ------------------------------------------------------------------
784 // Public properties — set from JS (use `.data=${...}` in templates).
785 // ------------------------------------------------------------------
786 /** The row buffer. Reassigning replaces (and clears expansion state). */
787 get data() {
788 return this._data;
789 }
790 set data(next) {
791 this._data = Array.isArray(next) ? next.slice() : [];
792 this._expanded.clear();
793 this._schedulePaint();
794 }
795 /** Column descriptors. See {@link WpdTableColumn}. */
796 get columns() {
797 return this._columns;
798 }
799 set columns(next) {
800 this._columns = Array.isArray(next) ? next.slice() : [];
801 const keys = new Set(this._columns.map((c) => c.key));
802 for (const k of Object.keys(this._filters)) {
803 if (!keys.has(k)) {
804 delete this._filters[k];
805 }
806 }
807 for (const k of Array.from(this._filterCache.keys())) {
808 if (!keys.has(k)) {
809 this._filterCache.delete(k);
810 }
811 }
812 if (this._sort && !keys.has(this._sort.key)) {
813 this._sort = null;
814 }
815 this._schedulePaint();
816 }
817 /** Read or replace the current filter map. */
818 get filters() {
819 return { ...this._filters };
820 }
821 set filters(next) {
822 this._filters = next ? { ...next } : {};
823 this._schedulePaint();
824 }
825 /** Read or set the active sort. `null` clears it. */
826 get sort() {
827 return this._sort ? { ...this._sort } : null;
828 }
829 set sort(next) {
830 this._sort = next ? { ...next } : null;
831 this._schedulePaint();
832 }
833 /** Read or replace the selection (set of row ids). */
834 get selection() {
835 return new Set(this._selection);
836 }
837 set selection(next) {
838 this._selection = new Set(next ?? []);
839 this._schedulePaint();
840 }
841 /** The currently-selected rows (resolved from `selection` + `data`). */
842 get selectedRows() {
843 const out = [];
844 this._data.forEach((row, i) => {
845 if (this._selection.has(this._getRowId(row, i))) {
846 out.push(row);
847 }
848 });
849 return out;
850 }
851 /**
852 * The rows currently visible — i.e. passing the active client-side
853 * filters, in data order. This is the row set `selectAll()` and
854 * the header select-all tri-state operate on.
855 *
856 * Destructive bulk consumers should resolve `selection` against
857 * THIS list rather than `data`: selection deliberately survives
858 * `data` reassignment, and a data-driven change (a realtime
859 * refresh editing a row so it no longer matches an active filter)
860 * can hide a selected row without any filter event firing. Rows
861 * the user cannot see must never be swept into a destructive
862 * action. See `collectSelectedItems()` in src/recycle-bin/index.ts
863 * for the canonical consumer.
864 *
865 * @since 0.9.4
866 */
867 get visibleRows() {
868 return this._filteredRows().map((entry) => entry.row);
869 }
870 /** Stable row-id extractor. Default is row index. */
871 get getRowId() {
872 return this._getRowId;
873 }
874 set getRowId(fn) {
875 this._getRowId = typeof fn === "function" ? fn : (_r, i) => i;
876 this._schedulePaint();
877 }
878 /**
879 * Sub-table accessor. Return `null` (or omit) for rows with no
880 * children. Return `{ columns, data }` to render a nested
881 * `<wpd-table>` inline; or return any `Node` / `html\`\`` template
882 * for fully custom expanded content.
883 */
884 get subTable() {
885 return this._subTable;
886 }
887 set subTable(fn) {
888 this._subTable = typeof fn === "function" ? fn : null;
889 this._expanded.clear();
890 this._schedulePaint();
891 }
892 /** Read or replace the expansion set (row indices that are open). */
893 get expanded() {
894 return new Set(this._expanded);
895 }
896 set expanded(next) {
897 this._expanded = new Set(next ?? []);
898 this._schedulePaint();
899 }
900 // ------------------------------------------------------------------
901 // Programmatic methods
902 // ------------------------------------------------------------------
903 /** Open a row's sub-table by index. No-op if the index is out of range. */
904 expand(index) {
905 if (index < 0 || index >= this._data.length) {
906 return;
907 }
908 if (this._expanded.has(index)) {
909 return;
910 }
911 this._expanded.add(index);
912 this.emit("wpd-table-expand-change", {
913 row: this._data[index],
914 index,
915 expanded: true
916 });
917 this._schedulePaint();
918 }
919 /** Close a row's sub-table by index. No-op if it wasn't open. */
920 collapse(index) {
921 if (!this._expanded.has(index)) {
922 return;
923 }
924 this._expanded.delete(index);
925 this.emit("wpd-table-expand-change", {
926 row: this._data[index],
927 index,
928 expanded: false
929 });
930 this._schedulePaint();
931 }
932 /** Open every row that has children. */
933 expandAll() {
934 if (!this._subTable) {
935 return;
936 }
937 let changed = false;
938 for (let i = 0; i < this._data.length; i++) {
939 if (!this._subTable(this._data[i], i)) {
940 continue;
941 }
942 if (!this._expanded.has(i)) {
943 this._expanded.add(i);
944 changed = true;
945 }
946 }
947 if (changed) {
948 this._schedulePaint();
949 }
950 }
951 /** Close every open row. */
952 collapseAll() {
953 if (this._expanded.size === 0) {
954 return;
955 }
956 this._expanded.clear();
957 this._schedulePaint();
958 }
959 isExpanded(index) {
960 return this._expanded.has(index);
961 }
962 /** Drop every active filter and emit `wpd-table-filter-change`. */
963 clearFilters() {
964 if (Object.keys(this._filters).length === 0) {
965 return;
966 }
967 this._filters = {};
968 this.emit("wpd-table-filter-change", { filters: {} });
969 this._schedulePaint();
970 }
971 /** Drop the active sort and emit `wpd-table-sort-change`. */
972 clearSort() {
973 if (this._sort === null) {
974 return;
975 }
976 this._sort = null;
977 this.emit("wpd-table-sort-change", { sort: null });
978 this._schedulePaint();
979 }
980 /**
981 * Add a row id to the selection. Emits `wpd-table-selection-change`.
982 *
983 * Selection mutators (`select` / `deselect` / `selectAll` /
984 * `clearSelection`) update the affected row in place via
985 * {@link _syncSelectionDom} rather than re-rendering the whole
986 * tbody — a rebuild would tear down the focused checkbox and
987 * (because scroll-anchoring abandons a momentarily empty container)
988 * could snap scroll back to the top.
989 */
990 select(id) {
991 if (this._selection.has(id)) {
992 return;
993 }
994 const mode = this._readSelectable();
995 const previouslySelected = mode === "single" ? Array.from(this._selection) : [];
996 if (mode === "single") {
997 this._selection.clear();
998 }
999 this._selection.add(id);
1000 this._emitSelectionChange();
1001 this._syncSelectionDom([id, ...previouslySelected]);
1002 }
1003 /** Remove a row id from the selection. */
1004 deselect(id) {
1005 if (!this._selection.delete(id)) {
1006 return;
1007 }
1008 this._emitSelectionChange();
1009 this._syncSelectionDom([id]);
1010 }
1011 /** Select every visible row — the rows passing the active client-side filters (multi-mode only). */
1012 selectAll() {
1013 if (this._readSelectable() !== "multi") {
1014 return;
1015 }
1016 for (const { row, index } of this._filteredRows()) {
1017 this._selection.add(this._getRowId(row, index));
1018 }
1019 this._emitSelectionChange();
1020 this._syncSelectionDom("all");
1021 }
1022 /** Empty the selection. */
1023 clearSelection() {
1024 if (this._selection.size === 0) {
1025 return;
1026 }
1027 this._selection.clear();
1028 this._emitSelectionChange();
1029 this._syncSelectionDom("all");
1030 }
1031 /**
1032 * Apply a selection change to the existing tbody DOM without
1033 * rebuilding it. Updates each affected row's `is-selected` class
1034 * and `select-row-checkbox` `checked` state, then re-syncs the
1035 * header select-all checkbox (checked / indeterminate / empty).
1036 *
1037 * @param ids `'all'` to walk every row, or an iterable of row ids
1038 * whose rows need updating. Unknown ids are silently
1039 * skipped (row may not be in the current filter/page).
1040 */
1041 _syncSelectionDom(ids) {
1042 const root = this.shadowRoot;
1043 if (!root) {
1044 return;
1045 }
1046 const tbody = root.querySelector("tbody");
1047 if (!tbody) {
1048 return;
1049 }
1050 let needle = null;
1051 if (ids !== "all") {
1052 needle = /* @__PURE__ */ new Set();
1053 for (const id of ids) {
1054 needle.add(String(id));
1055 }
1056 }
1057 const rows = tbody.querySelectorAll(
1058 "tr[data-row-id]"
1059 );
1060 for (const tr of rows) {
1061 const rowIdStr = tr.dataset.rowId;
1062 if (rowIdStr === void 0) {
1063 continue;
1064 }
1065 if (needle && !needle.has(rowIdStr)) {
1066 continue;
1067 }
1068 const idx = Number(tr.dataset.rowIndex);
1069 if (!Number.isFinite(idx)) {
1070 continue;
1071 }
1072 const row = this._data[idx];
1073 if (row === void 0) {
1074 continue;
1075 }
1076 const id = this._getRowId(row, idx);
1077 const isSelected = this._selection.has(id);
1078 tr.classList.toggle("is-selected", isSelected);
1079 const cb = tr.querySelector(
1080 "input.select-row-checkbox"
1081 );
1082 if (cb && cb.checked !== isSelected) {
1083 cb.checked = isSelected;
1084 }
1085 }
1086 const headerCb = root.querySelector(
1087 "thead .select-all-checkbox"
1088 );
1089 if (headerCb) {
1090 const { total, selected } = this._visibleSelectionStats();
1091 headerCb.checked = total > 0 && selected === total;
1092 headerCb.indeterminate = selected > 0 && selected < total;
1093 }
1094 }
1095 /** Scroll the (filtered) row at `index` into view inside the table's scroll container. */
1096 scrollToRow(index) {
1097 const root = this.shadowRoot;
1098 if (!root) {
1099 return;
1100 }
1101 const rows = root.querySelectorAll(
1102 "tbody tr:not(.subtable):not(.empty):not(.skeleton)"
1103 );
1104 const row = rows[index];
1105 if (row) {
1106 row.scrollIntoView({ block: "nearest", inline: "nearest" });
1107 }
1108 }
1109 connectedCallback() {
1110 super.connectedCallback();
1111 this._schedulePaint();
1112 }
1113 disconnectedCallback() {
1114 this._resizeObserver?.disconnect();
1115 this._resizeObserver = null;
1116 if (this._stickyRafHandle !== null && typeof cancelAnimationFrame !== "undefined") {
1117 cancelAnimationFrame(this._stickyRafHandle);
1118 this._stickyRafHandle = null;
1119 }
1120 }
1121 /**
1122 * Force a sticky-offsets recompute. Public escape hatch for the
1123 * rare case where layout settles after every internal hook has
1124 * fired — e.g. an out-of-band font swap or a JS-driven width
1125 * change on an ancestor that doesn't bubble through ResizeObserver.
1126 *
1127 * Usually you don't need this: the component schedules recomputes
1128 * on a microtask + animation frame after every paint, and a
1129 * ResizeObserver on the inner scroll element catches geometry
1130 * changes thereafter. Reach for `recomputeLayout()` only if you've
1131 * confirmed that all of those pathways missed your case.
1132 */
1133 recomputeLayout() {
1134 this._applyStickyOffsets();
1135 this._measureHeaderHeight();
1136 }
1137 // ------------------------------------------------------------------
1138 // Skeleton + paint pipeline
1139 // ------------------------------------------------------------------
1140 render() {
1141 return html`
1142 <div class="scroll" part="scroll">
1143 <table part="table">
1144 <colgroup></colgroup>
1145 <thead></thead>
1146 <tbody></tbody>
1147 </table>
1148 </div>
1149 `;
1150 }
1151 requestUpdate() {
1152 super.requestUpdate();
1153 this._schedulePaint();
1154 }
1155 _schedulePaint() {
1156 if (this._paintScheduled || !this.isConnected) {
1157 return;
1158 }
1159 this._paintScheduled = true;
1160 queueMicrotask(() => {
1161 this._paintScheduled = false;
1162 if (!this.isConnected) {
1163 return;
1164 }
1165 this._paint();
1166 });
1167 }
1168 _paint() {
1169 const root = this.shadowRoot;
1170 if (!root) {
1171 return;
1172 }
1173 if (!root.querySelector("tbody")) {
1174 render(this.render(), root);
1175 }
1176 const colgroup = root.querySelector("colgroup");
1177 const thead = root.querySelector("thead");
1178 const tbody = root.querySelector("tbody");
1179 if (!colgroup || !thead || !tbody) {
1180 return;
1181 }
1182 const cols = this._effectiveColumns();
1183 const stickyN = this._readStickyColumns();
1184 this._lastStickyIndex = this._computeLastStickyIndex(cols, stickyN);
1185 this._paintColgroup(colgroup, cols);
1186 this._paintHead(thead, cols, stickyN);
1187 this._paintBody(tbody, cols, stickyN);
1188 this._applyStickyOffsets();
1189 this._measureHeaderHeight();
1190 this._scheduleStickyOffsets();
1191 this._maybeWarnStickyHeader();
1192 this._maybeWarnLoadingDesync(tbody);
1193 this._ensureResizeObserver();
1194 }
1195 /**
1196 * Diagnostic for the "I set `loading` but the skeleton never
1197 * appeared" footgun. If we get here with the attribute on but no
1198 * `.skeleton` rows in `tbody`, something between attribute set and
1199 * paint went off the rails — historically this happened when the
1200 * base `Component.attributeChangedCallback` called `_scheduleRender`
1201 * directly, bypassing our `requestUpdate` override. Same pattern as
1202 * the sticky-columns 0px tripwire: should never fire, but if it
1203 * does, names the bug instead of leaving the dev guessing.
1204 */
1205 _maybeWarnLoadingDesync(tbody) {
1206 if (this._loadingDesyncWarned) {
1207 return;
1208 }
1209 if (!this.hasAttribute("loading")) {
1210 return;
1211 }
1212 if (tbody.querySelector("tr.skeleton")) {
1213 return;
1214 }
1215 this._loadingDesyncWarned = true;
1216 console.warn(
1217 "[wpd-table] `loading` attribute is set but no skeleton rows rendered. Either attributeChangedCallback didn't route through requestUpdate (framework regression), or `loading` was set after the most recent paint and no follow-up trigger ran. Toggling `data` will force a paint as a workaround."
1218 );
1219 }
1220 /**
1221 * Belt-and-braces sticky-offset scheduling.
1222 *
1223 * - Microtask: cheap, fires after the current task drains. Fixes
1224 * mounts where the synchronous read in `_paint` happened before
1225 * a sibling style applied.
1226 * - rAF: fires before the next paint. Catches "layout settles
1227 * after a queued style mutation" races — the most common cause
1228 * of "col 1 ended up at inset-inline-start: 0px".
1229 *
1230 * Both reduce to a no-op when nothing changed. The cost is two
1231 * extra DOM reads per paint; the win is the bug class disappears.
1232 */
1233 _scheduleStickyOffsets() {
1234 if (!this._stickyMicroScheduled) {
1235 this._stickyMicroScheduled = true;
1236 queueMicrotask(() => {
1237 this._stickyMicroScheduled = false;
1238 if (this.isConnected) {
1239 this._applyStickyOffsets();
1240 }
1241 });
1242 }
1243 if (this._stickyRafHandle === null && typeof requestAnimationFrame !== "undefined") {
1244 this._stickyRafHandle = requestAnimationFrame(() => {
1245 this._stickyRafHandle = null;
1246 if (this.isConnected) {
1247 this._applyStickyOffsets();
1248 this._measureHeaderHeight();
1249 }
1250 });
1251 }
1252 }
1253 /**
1254 * Wire a `ResizeObserver` on the inner `.scroll` element (NOT the
1255 * host). Why: the host's outer width is often pinned by its parent
1256 * panel — a vertical scrollbar appearing inside the table changes
1257 * the inner scroll-area width by ~15px without changing the host
1258 * size. Observing the host would miss that reflow and leave sticky
1259 * offsets stale.
1260 *
1261 * Idempotent — runs once after the first paint produces a real
1262 * `.scroll` element. Disconnect happens in `disconnectedCallback`.
1263 */
1264 _ensureResizeObserver() {
1265 if (this._resizeObserver) {
1266 return;
1267 }
1268 if (typeof ResizeObserver === "undefined") {
1269 return;
1270 }
1271 const scroll = this.shadowRoot?.querySelector(
1272 ".scroll"
1273 );
1274 if (!scroll) {
1275 return;
1276 }
1277 this._resizeObserver = new ResizeObserver(() => {
1278 if (!this.isConnected) {
1279 return;
1280 }
1281 this._applyStickyOffsets();
1282 this._measureHeaderHeight();
1283 this._stickyHeaderWarned = false;
1284 this._maybeWarnStickyHeader();
1285 });
1286 this._resizeObserver.observe(scroll);
1287 this._resizeObserver.observe(this);
1288 }
1289 _paintColgroup(colgroup, cols) {
1290 const out = [];
1291 for (const c of cols) {
1292 const col = document.createElement("col");
1293 if (c.width) {
1294 col.style.width = c.width;
1295 }
1296 out.push(col);
1297 }
1298 colgroup.replaceChildren(...out);
1299 }
1300 _paintHead(thead, cols, stickyN) {
1301 const newHeaderRow = document.createElement("tr");
1302 newHeaderRow.setAttribute("part", "header-row");
1303 for (let i = 0; i < cols.length; i++) {
1304 newHeaderRow.appendChild(this._buildHeaderCell(cols[i], i, stickyN));
1305 }
1306 const existingHeader = thead.querySelector(
1307 ':scope > tr[part="header-row"]'
1308 );
1309 if (existingHeader) {
1310 thead.replaceChild(newHeaderRow, existingHeader);
1311 } else {
1312 thead.insertBefore(newHeaderRow, thead.firstChild);
1313 }
1314 const hasFilter = cols.some(
1315 (c) => c.filter || Array.isArray(c.filterOptions) || typeof c.filterRender === "function"
1316 );
1317 let existingFilter = thead.querySelector(
1318 ":scope > tr.filter-row"
1319 );
1320 if (hasFilter) {
1321 const cells = [];
1322 for (let i = 0; i < cols.length; i++) {
1323 cells.push(this._buildFilterCell(cols[i], i, stickyN));
1324 }
1325 if (!existingFilter) {
1326 existingFilter = document.createElement("tr");
1327 existingFilter.classList.add("filter-row");
1328 existingFilter.setAttribute("part", "filter-row");
1329 thead.appendChild(existingFilter);
1330 }
1331 const current = Array.from(existingFilter.children);
1332 let same = current.length === cells.length;
1333 if (same) {
1334 for (let i = 0; i < cells.length; i++) {
1335 if (current[i] !== cells[i]) {
1336 same = false;
1337 break;
1338 }
1339 }
1340 }
1341 if (!same) {
1342 const wanted = new Set(cells);
1343 for (const cell of cells) {
1344 existingFilter.appendChild(cell);
1345 }
1346 for (const child of Array.from(existingFilter.children)) {
1347 if (!wanted.has(child)) {
1348 existingFilter.removeChild(child);
1349 }
1350 }
1351 }
1352 } else if (existingFilter) {
1353 existingFilter.remove();
1354 }
1355 }
1356 _buildHeaderCell(col, index, stickyN) {
1357 const th = document.createElement("th");
1358 th.setAttribute("scope", "col");
1359 th.dataset.key = col.key;
1360 this._applyCellClasses(th, col, index, stickyN);
1361 if (col.minWidth) {
1362 th.style.minWidth = col.minWidth;
1363 }
1364 if (col.key === SELECT_KEY) {
1365 const mode = this._readSelectable();
1366 if (mode === "multi") {
1367 const cb = document.createElement("input");
1368 cb.type = "checkbox";
1369 cb.className = "select-all-checkbox";
1370 cb.setAttribute("data-noclick", "");
1371 cb.setAttribute("aria-label", "Select all rows");
1372 const { total, selected } = this._visibleSelectionStats();
1373 cb.checked = total > 0 && selected === total;
1374 cb.indeterminate = selected > 0 && selected < total;
1375 cb.addEventListener("change", () => {
1376 if (cb.checked) {
1377 this.selectAll();
1378 } else {
1379 this.clearSelection();
1380 }
1381 });
1382 th.appendChild(cb);
1383 }
1384 return th;
1385 }
1386 th.textContent = col.label ?? (col.key === EXPANDER_KEY ? "" : col.key);
1387 if (col.sortable) {
1388 th.classList.add("is-sortable");
1389 const isActive = this._sort?.key === col.key;
1390 const indicator = document.createElement("span");
1391 indicator.className = "sort-indicator";
1392 let arrow = "";
1393 if (isActive) {
1394 arrow = this._sort.direction === "asc" ? "" : "";
1395 }
1396 indicator.textContent = arrow;
1397 th.appendChild(indicator);
1398 if (isActive) {
1399 th.classList.add(
1400 this._sort.direction === "asc" ? "sort-asc" : "sort-desc"
1401 );
1402 }
1403 th.addEventListener("click", () => this._cycleSort(col.key));
1404 }
1405 return th;
1406 }
1407 _buildFilterCell(col, index, stickyN) {
1408 const cached = this._filterCache.get(col.key);
1409 const hasExplicitOptions = Array.isArray(col.filterOptions);
1410 const hasCustomRender = typeof col.filterRender === "function";
1411 let desiredKind;
1412 if (!col.filter && !hasExplicitOptions && !hasCustomRender || col.key === EXPANDER_KEY || col.key === SELECT_KEY) {
1413 desiredKind = "none";
1414 } else if (hasCustomRender) {
1415 desiredKind = "custom";
1416 } else if (col.filter === "select" || hasExplicitOptions) {
1417 desiredKind = "select";
1418 } else {
1419 desiredKind = "text";
1420 }
1421 if (cached && cached.kind === desiredKind) {
1422 cached.th.className = "";
1423 this._applyCellClasses(cached.th, col, index, stickyN);
1424 if (desiredKind === "select") {
1425 const select = cached.control;
1426 const opts = this._resolveFilterOptions(col);
1427 const optsKey = opts.map((o) => o.value).join("|");
1428 if (optsKey !== cached.optionsKey) {
1429 this._populateSelect(select, opts, this._filters[col.key] ?? "");
1430 cached.optionsKey = optsKey;
1431 } else {
1432 select.value = this._filters[col.key] ?? "";
1433 }
1434 } else if (desiredKind === "text") {
1435 const input = cached.control;
1436 const want = this._filters[col.key] ?? "";
1437 if (input.value !== want && input.ownerDocument.activeElement !== input) {
1438 input.value = want;
1439 }
1440 } else if (desiredKind === "custom" && col.filterRender) {
1441 col.filterRender(cached.th, {
1442 value: this._filters[col.key] ?? "",
1443 setValue: (next) => this._onFilterChange(col.key, next),
1444 col
1445 });
1446 }
1447 return cached.th;
1448 }
1449 const th = document.createElement("th");
1450 this._applyCellClasses(th, col, index, stickyN);
1451 if (desiredKind === "none") {
1452 this._filterCache.set(col.key, {
1453 th,
1454 control: null,
1455 optionsKey: "",
1456 kind: "none"
1457 });
1458 return th;
1459 }
1460 if (desiredKind === "custom" && col.filterRender) {
1461 col.filterRender(th, {
1462 value: this._filters[col.key] ?? "",
1463 setValue: (next) => this._onFilterChange(col.key, next),
1464 col
1465 });
1466 this._filterCache.set(col.key, {
1467 th,
1468 control: null,
1469 optionsKey: "",
1470 kind: "custom"
1471 });
1472 return th;
1473 }
1474 let control;
1475 let optionsKey = "";
1476 if (desiredKind === "select") {
1477 const select = document.createElement("select");
1478 select.classList.add("filter-select");
1479 select.setAttribute("data-noclick", "");
1480 select.setAttribute(
1481 "aria-label",
1482 `Filter ${col.label ?? col.key}`
1483 );
1484 const opts = this._resolveFilterOptions(col);
1485 this._populateSelect(select, opts, this._filters[col.key] ?? "");
1486 optionsKey = opts.map((o) => o.value).join("|");
1487 select.addEventListener("change", () => {
1488 this._onFilterChange(col.key, select.value);
1489 });
1490 control = select;
1491 } else {
1492 const input = document.createElement("input");
1493 input.type = "search";
1494 input.classList.add("filter-input");
1495 input.setAttribute("data-noclick", "");
1496 input.setAttribute("placeholder", "Filter…");
1497 input.setAttribute("aria-label", `Filter ${col.label ?? col.key}`);
1498 input.value = this._filters[col.key] ?? "";
1499 input.addEventListener("input", () => {
1500 this._onFilterChange(col.key, input.value);
1501 });
1502 control = input;
1503 }
1504 th.appendChild(control);
1505 this._filterCache.set(col.key, {
1506 th,
1507 control,
1508 optionsKey,
1509 kind: desiredKind
1510 });
1511 return th;
1512 }
1513 _populateSelect(select, options, current) {
1514 select.replaceChildren();
1515 const all = document.createElement("option");
1516 all.value = "";
1517 all.textContent = "All";
1518 select.appendChild(all);
1519 for (const opt of options) {
1520 const el = document.createElement("option");
1521 el.value = opt.value;
1522 el.textContent = opt.label;
1523 if (opt.value === current) {
1524 el.selected = true;
1525 }
1526 select.appendChild(el);
1527 }
1528 select.value = current;
1529 }
1530 /**
1531 * Resolve the option list for a select-filter column. Explicit
1532 * `filterOptions` win — that's the contract for server-driven
1533 * tables that need the dropdown to list values not present on
1534 * the current page. Without `filterOptions`, fall back to the
1535 * unique row values in the column (legacy behaviour for
1536 * client-side tables).
1537 */
1538 _resolveFilterOptions(col) {
1539 if (Array.isArray(col.filterOptions)) {
1540 return col.filterOptions;
1541 }
1542 return this._uniqueValues(col.key).map((v) => ({
1543 value: v,
1544 label: v
1545 }));
1546 }
1547 // ------------------------------------------------------------------
1548 // Body
1549 // ------------------------------------------------------------------
1550 _paintBody(tbody, cols, stickyN) {
1551 tbody.replaceChildren();
1552 if (this.hasAttribute("loading")) {
1553 const count = this._readLoadingRows();
1554 for (let i = 0; i < count; i++) {
1555 tbody.appendChild(this._buildSkeletonRow(cols, i));
1556 }
1557 return;
1558 }
1559 const filtered = this._sortedRows(this._filteredRows());
1560 if (filtered.length === 0) {
1561 tbody.appendChild(this._buildEmptyRow(cols.length));
1562 return;
1563 }
1564 for (const { row, index } of filtered) {
1565 tbody.appendChild(this._buildBodyRow(row, index, cols, stickyN));
1566 if (this._expanded.has(index) && this._subTable) {
1567 const sub = this._subTable(row, index);
1568 if (sub) {
1569 tbody.appendChild(this._buildSubTableRow(sub, cols.length));
1570 }
1571 }
1572 }
1573 }
1574 _buildEmptyRow(colspan) {
1575 const tr = document.createElement("tr");
1576 tr.classList.add("empty");
1577 const td = document.createElement("td");
1578 td.colSpan = colspan;
1579 const slot = document.createElement("slot");
1580 slot.name = "empty";
1581 slot.textContent = this.getAttribute("empty") || "No data";
1582 td.appendChild(slot);
1583 tr.appendChild(td);
1584 return tr;
1585 }
1586 _buildSkeletonRow(cols, seed) {
1587 const tr = document.createElement("tr");
1588 tr.classList.add("skeleton");
1589 tr.setAttribute("aria-hidden", "true");
1590 for (const _c of cols) {
1591 const td = document.createElement("td");
1592 const bar = document.createElement("span");
1593 bar.className = "skeleton-bar";
1594 const widthPct = 50 + (seed * 7 + tr.children.length * 13) % 40;
1595 bar.style.width = `${widthPct}%`;
1596 td.appendChild(bar);
1597 tr.appendChild(td);
1598 }
1599 return tr;
1600 }
1601 _buildBodyRow(row, rowIndex, cols, stickyN) {
1602 const tr = document.createElement("tr");
1603 tr.setAttribute("part", "row");
1604 tr.dataset.rowIndex = String(rowIndex);
1605 const id = this._getRowId(row, rowIndex);
1606 tr.dataset.rowId = String(id);
1607 if (this._selection.has(id)) {
1608 tr.classList.add("is-selected");
1609 }
1610 tr.addEventListener("click", (e) => {
1611 this._onRowClick(row, rowIndex, e);
1612 });
1613 for (let i = 0; i < cols.length; i++) {
1614 tr.appendChild(
1615 this._buildBodyCell(cols[i], i, row, rowIndex, stickyN)
1616 );
1617 }
1618 return tr;
1619 }
1620 _buildBodyCell(col, colIndex, row, rowIndex, stickyN) {
1621 const td = document.createElement("td");
1622 this._applyCellClasses(td, col, colIndex, stickyN);
1623 if (col.minWidth) {
1624 td.style.minWidth = col.minWidth;
1625 }
1626 if (col.key === SELECT_KEY) {
1627 const id = this._getRowId(row, rowIndex);
1628 const cb = document.createElement("input");
1629 cb.type = "checkbox";
1630 cb.className = "select-row-checkbox";
1631 cb.setAttribute("data-noclick", "");
1632 cb.setAttribute("aria-label", "Select row");
1633 cb.checked = this._selection.has(id);
1634 cb.addEventListener("change", () => {
1635 if (cb.checked) {
1636 this.select(id);
1637 } else {
1638 this.deselect(id);
1639 }
1640 });
1641 td.appendChild(cb);
1642 return td;
1643 }
1644 if (col.key === EXPANDER_KEY) {
1645 const hasChildren = this._subTable ? !!this._subTable(row, rowIndex) : false;
1646 if (!hasChildren) {
1647 return td;
1648 }
1649 const isOpen = this._expanded.has(rowIndex);
1650 const btn = document.createElement("button");
1651 btn.type = "button";
1652 btn.className = "expander";
1653 btn.setAttribute("data-noclick", "");
1654 btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
1655 btn.setAttribute(
1656 "aria-label",
1657 isOpen ? "Collapse row" : "Expand row"
1658 );
1659 btn.textContent = isOpen ? "" : "";
1660 btn.addEventListener("click", (e) => {
1661 this._toggleRow(rowIndex, row, e);
1662 });
1663 td.appendChild(btn);
1664 return td;
1665 }
1666 const value = row[col.key];
1667 if (col.render) {
1668 const out = col.render(value, row, rowIndex);
1669 this._mountCellContent(td, out);
1670 } else if (value !== null && value !== void 0) {
1671 td.textContent = String(value);
1672 }
1673 return td;
1674 }
1675 _buildSubTableRow(sub, colspan) {
1676 const tr = document.createElement("tr");
1677 tr.classList.add("subtable");
1678 tr.setAttribute("part", "subtable-row");
1679 const td = document.createElement("td");
1680 td.colSpan = colspan;
1681 const inner = document.createElement("div");
1682 inner.classList.add("subtable-inner");
1683 if (sub instanceof Node) {
1684 inner.appendChild(sub);
1685 } else if (isTemplateResult(sub)) {
1686 render(sub, inner);
1687 } else {
1688 const nested = document.createElement("wpd-table");
1689 nested.columns = sub.columns;
1690 nested.data = sub.data;
1691 if (sub.subTable) {
1692 nested.subTable = sub.subTable;
1693 }
1694 inner.appendChild(nested);
1695 }
1696 td.appendChild(inner);
1697 tr.appendChild(td);
1698 return tr;
1699 }
1700 _mountCellContent(td, out) {
1701 if (typeof out === "string") {
1702 td.textContent = out;
1703 return;
1704 }
1705 if (out instanceof Node) {
1706 td.appendChild(out);
1707 return;
1708 }
1709 if (isTemplateResult(out)) {
1710 render(out, td);
1711 }
1712 }
1713 // ------------------------------------------------------------------
1714 // Behavior
1715 // ------------------------------------------------------------------
1716 _onFilterChange(key, value) {
1717 if (value === "") {
1718 delete this._filters[key];
1719 } else {
1720 this._filters[key] = value;
1721 }
1722 this.emit("wpd-table-filter-change", { filters: { ...this._filters } });
1723 const root = this.shadowRoot;
1724 const tbody = root?.querySelector("tbody");
1725 if (tbody) {
1726 const cols = this._effectiveColumns();
1727 const stickyN = this._readStickyColumns();
1728 this._lastStickyIndex = this._computeLastStickyIndex(cols, stickyN);
1729 this._paintBody(tbody, cols, stickyN);
1730 this._applyStickyOffsets();
1731 }
1732 }
1733 _onRowClick(row, index, e) {
1734 const path = e.composedPath?.() ?? [];
1735 for (const node of path) {
1736 if (node instanceof Element && node.hasAttribute("data-noclick")) {
1737 return;
1738 }
1739 if (node === this) {
1740 break;
1741 }
1742 }
1743 this.emit("wpd-table-row-click", { row, index, originalEvent: e });
1744 }
1745 _toggleRow(index, row, e) {
1746 e.stopPropagation();
1747 const isOpen = this._expanded.has(index);
1748 if (isOpen) {
1749 this._expanded.delete(index);
1750 } else {
1751 this._expanded.add(index);
1752 }
1753 this.emit("wpd-table-expand-change", {
1754 row,
1755 index,
1756 expanded: !isOpen
1757 });
1758 this._schedulePaint();
1759 }
1760 _cycleSort(key) {
1761 if (!this._sort || this._sort.key !== key) {
1762 this._sort = { key, direction: "asc" };
1763 } else if (this._sort.direction === "asc") {
1764 this._sort = { key, direction: "desc" };
1765 } else {
1766 this._sort = null;
1767 }
1768 this.emit("wpd-table-sort-change", {
1769 sort: this._sort ? { ...this._sort } : null
1770 });
1771 this._schedulePaint();
1772 }
1773 _emitSelectionChange() {
1774 this.emit("wpd-table-selection-change", {
1775 selection: Array.from(this._selection),
1776 rows: this.selectedRows
1777 });
1778 }
1779 // ------------------------------------------------------------------
1780 // Filtering + sorting
1781 // ------------------------------------------------------------------
1782 _filteredRows() {
1783 const out = [];
1784 const active = Object.keys(this._filters).filter(
1785 (k) => this._filters[k] !== ""
1786 );
1787 for (let i = 0; i < this._data.length; i++) {
1788 const row = this._data[i];
1789 let pass = true;
1790 for (const key of active) {
1791 const col = this._columns.find((c) => c.key === key);
1792 if (col && typeof col.filterRender === "function") {
1793 continue;
1794 }
1795 const filter = this._filters[key] ?? "";
1796 const cell = row[key];
1797 const cellStr = cell === null || cell === void 0 ? "" : String(cell);
1798 if (col?.filter === "select") {
1799 if (cellStr !== filter) {
1800 pass = false;
1801 break;
1802 }
1803 } else if (!cellStr.toLowerCase().includes(filter.toLowerCase())) {
1804 pass = false;
1805 break;
1806 }
1807 }
1808 if (pass) {
1809 out.push({ row, index: i });
1810 }
1811 }
1812 return out;
1813 }
1814 _sortedRows(rows) {
1815 if (!this._sort) {
1816 return rows;
1817 }
1818 const col = this._columns.find((c) => c.key === this._sort.key);
1819 if (!col) {
1820 return rows;
1821 }
1822 const dir = this._sort.direction === "desc" ? -1 : 1;
1823 const out = rows.slice();
1824 out.sort((a, b) => {
1825 const av = col.sortValue ? col.sortValue(a.row, a.row[col.key]) : a.row[col.key];
1826 const bv = col.sortValue ? col.sortValue(b.row, b.row[col.key]) : b.row[col.key];
1827 return compareValues(av, bv) * dir;
1828 });
1829 return out;
1830 }
1831 _uniqueValues(key) {
1832 const seen = /* @__PURE__ */ new Set();
1833 for (const row of this._data) {
1834 const v = row[key];
1835 if (v === null || v === void 0) {
1836 continue;
1837 }
1838 seen.add(String(v));
1839 }
1840 return Array.from(seen).sort();
1841 }
1842 /**
1843 * Selection stats over the VISIBLE (client-side-filtered) rows —
1844 * the same set `selectAll()` operates on. The header select-all
1845 * tri-state derives from these so "checked" always means "every
1846 * row the user can see is selected", even while ids of currently
1847 * hidden rows linger in the selection set.
1848 */
1849 _visibleSelectionStats() {
1850 let total = 0;
1851 let selected = 0;
1852 for (const { row, index } of this._filteredRows()) {
1853 total++;
1854 if (this._selection.has(this._getRowId(row, index))) {
1855 selected++;
1856 }
1857 }
1858 return { total, selected };
1859 }
1860 // ------------------------------------------------------------------
1861 // Sticky columns + attribute reads
1862 // ------------------------------------------------------------------
1863 _readStickyColumns() {
1864 const raw = parseInt(this.getAttribute("sticky-columns") || "0", 10);
1865 return Number.isFinite(raw) && raw > 0 ? raw : 0;
1866 }
1867 _readLoadingRows() {
1868 const raw = parseInt(this.getAttribute("loading-rows") || "5", 10);
1869 return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 100) : 5;
1870 }
1871 _readSelectable() {
1872 const v = this.getAttribute("selectable");
1873 if (v === "single") {
1874 return "single";
1875 }
1876 if (v === "multi" || v === "") {
1877 return "multi";
1878 }
1879 return null;
1880 }
1881 /**
1882 * Sticky-band membership. The first N columns get pinned, with two
1883 * per-column overrides: `column.sticky = true` opts in even outside
1884 * the band; `column.sticky = false` opts out within it.
1885 */
1886 _isStickyIndex(index, stickyN, col) {
1887 if (col.sticky === false) {
1888 return false;
1889 }
1890 if (col.sticky === true) {
1891 return true;
1892 }
1893 return index < stickyN;
1894 }
1895 _computeLastStickyIndex(cols, stickyN) {
1896 let last = -1;
1897 for (let i = 0; i < cols.length; i++) {
1898 if (this._isStickyIndex(i, stickyN, cols[i])) {
1899 last = i;
1900 }
1901 }
1902 return last;
1903 }
1904 _applyCellClasses(cell, col, index, stickyN) {
1905 if (col.key === EXPANDER_KEY) {
1906 cell.classList.add("col-expander");
1907 }
1908 if (col.key === SELECT_KEY) {
1909 cell.classList.add("col-select");
1910 }
1911 if (col.align === "center") {
1912 cell.classList.add("align-center");
1913 }
1914 if (col.align === "end") {
1915 cell.classList.add("align-end");
1916 }
1917 const sticky = this._isStickyIndex(index, stickyN, col);
1918 if (sticky) {
1919 cell.classList.add("is-sticky");
1920 if (index === this._lastStickyIndex) {
1921 cell.classList.add("is-sticky-edge");
1922 }
1923 }
1924 }
1925 _effectiveColumns() {
1926 const out = [];
1927 if (this._readSelectable()) {
1928 out.push({
1929 key: SELECT_KEY,
1930 label: "",
1931 // The descriptor width is painted onto a `<col>`
1932 // element and is the authoritative column-width
1933 // source in table-layout: auto — CSS `td { width }`
1934 // is ignored once `<col>` has a value. Pair with
1935 // the matching `td.col-select` rule (zero
1936 // `padding-inline`, `text-align: center`) so the
1937 // checkbox sits with breathing room on both sides.
1938 width: "40px",
1939 align: "center"
1940 });
1941 }
1942 if (this._subTable) {
1943 out.push({
1944 key: EXPANDER_KEY,
1945 label: "",
1946 // Same contract as col-select. 36px column +
1947 // 20px button + zero padding centers the chevron
1948 // with ~8px on each side.
1949 width: "36px",
1950 align: "center"
1951 });
1952 }
1953 out.push(...this._columns);
1954 return out;
1955 }
1956 /**
1957 * Walk the header row, sum the natural widths of the sticky cells,
1958 * then write cumulative `inset-inline-start` offsets onto every
1959 * row's matching cells.
1960 */
1961 _applyStickyOffsets() {
1962 const root = this.shadowRoot;
1963 if (!root) {
1964 return;
1965 }
1966 const headRow = root.querySelector("thead tr");
1967 if (!headRow) {
1968 return;
1969 }
1970 const ths = Array.from(headRow.children);
1971 const offsets = [];
1972 let acc = 0;
1973 for (let i = 0; i < ths.length; i++) {
1974 offsets[i] = acc;
1975 if (ths[i].classList.contains("is-sticky")) {
1976 acc += ths[i].offsetWidth;
1977 }
1978 }
1979 const rows = root.querySelectorAll(
1980 "thead tr, tbody tr:not(.subtable):not(.empty):not(.skeleton)"
1981 );
1982 rows.forEach((r) => {
1983 const cells = Array.from(r.children);
1984 for (let i = 0; i < cells.length; i++) {
1985 if (cells[i].classList.contains("is-sticky")) {
1986 cells[i].style.insetInlineStart = `${offsets[i]}px`;
1987 }
1988 }
1989 });
1990 this._maybeWarnStickyOffsetRace(ths, offsets);
1991 }
1992 _maybeWarnStickyOffsetRace(ths, offsets) {
1993 if (this._stickyRaceWarned) {
1994 return;
1995 }
1996 const stickyN = this._readStickyColumns();
1997 if (stickyN < 2) {
1998 return;
1999 }
2000 const lastIdx = Math.min(stickyN - 1, ths.length - 1);
2001 if (lastIdx <= 0) {
2002 return;
2003 }
2004 if (offsets[lastIdx] !== 0) {
2005 return;
2006 }
2007 if (this.offsetWidth === 0) {
2008 return;
2009 }
2010 this._stickyRaceWarned = true;
2011 const w0 = ths[0]?.offsetWidth ?? 0;
2012 console.warn(
2013 `[wpd-table] sticky-columns: column ${lastIdx} resolved to inset-inline-start: 0px while the host is visible. ths[0].offsetWidth was ${w0}px at measurement time. Likely a layout race — call recomputeLayout() after the panel finishes its mount/transition, or wrap the assignment of \`data\` in a requestAnimationFrame.`
2014 );
2015 }
2016 _measureHeaderHeight() {
2017 const root = this.shadowRoot;
2018 if (!root) {
2019 return;
2020 }
2021 const headRow = root.querySelector("thead tr");
2022 if (!headRow) {
2023 return;
2024 }
2025 const h = headRow.offsetHeight;
2026 if (h > 0) {
2027 this.style.setProperty("--wpd-table-header-height", `${h}px`);
2028 }
2029 }
2030 /**
2031 * Once-per-element warning for the most common sticky-header
2032 * mistake: forgetting to give the table a scroll container. Without
2033 * a max-height (or a scrolling ancestor), `position: sticky`
2034 * silently does nothing because there's no scrollport for it to
2035 * stick within.
2036 */
2037 _maybeWarnStickyHeader() {
2038 if (this._stickyHeaderWarned) {
2039 return;
2040 }
2041 if (!this.hasAttribute("sticky-header")) {
2042 return;
2043 }
2044 if (this.hasAttribute("loading") || this._data.length < 8) {
2045 return;
2046 }
2047 const scroll = this.shadowRoot?.querySelector(
2048 ".scroll"
2049 );
2050 if (!scroll) {
2051 return;
2052 }
2053 if (scroll.offsetWidth === 0) {
2054 return;
2055 }
2056 if (scroll.scrollHeight <= scroll.clientHeight + 1) {
2057 this._stickyHeaderWarned = true;
2058 console.warn(
2059 "[wpd-table] sticky-header is set but the table has no scroll container. Set --wpd-table-max-height on the host (or wrap it in a scrolling parent) so the header has something to stick to."
2060 );
2061 }
2062 }
2063 };
2064 _WpdTable.props = [
2065 "stickyColumns",
2066 "stickyHeader",
2067 "striped",
2068 "hover",
2069 "compact",
2070 "bordered",
2071 "empty",
2072 "loading",
2073 "loadingRows",
2074 "selectable"
2075 ];
2076 _WpdTable.styles = [styles$c];
2077 _WpdTable.help = {
2078 title: "Table",
2079 summary: "Data-driven table. Assign `columns` + `data` and you get a styled table with optional per-column filters, click-to-sort, multi-row selection, sticky columns/header, sub-tables, custom cell renderers, loading skeleton, and a slottable empty state.",
2080 status: "experimental",
2081 since: "0.6.0",
2082 props: [
2083 {
2084 name: "sticky-columns",
2085 type: "integer",
2086 description: "Pin the first N columns to the inline-start edge. Widths are measured after layout, so variable-width columns work. The auto-injected expander (subTable) and select (selectable) columns count toward N."
2087 },
2088 {
2089 name: "sticky-header",
2090 type: "boolean",
2091 description: "Pin the header (and filter row) to the top. Requires a scrolling parent or `--wpd-table-max-height` — the component warns once if it detects sticky-header on a non-scrolling container."
2092 },
2093 { name: "striped", type: "boolean", description: "Zebra rows." },
2094 { name: "hover", type: "boolean", description: "Highlight rows on hover." },
2095 { name: "compact", type: "boolean", description: "Tighter padding + smaller font." },
2096 { name: "bordered", type: "boolean", description: "Vertical cell borders." },
2097 {
2098 name: "empty",
2099 type: "string",
2100 description: "Fallback text shown when there are no rows. For richer empty states, project light-DOM content into the `empty` slot."
2101 },
2102 {
2103 name: "loading",
2104 type: "boolean",
2105 description: "Paint shimmering skeleton rows in place of body content. Filters / sort headers stay live."
2106 },
2107 {
2108 name: "loading-rows",
2109 type: "integer",
2110 description: "Number of skeleton rows when loading. Default 5."
2111 },
2112 {
2113 name: "selectable",
2114 type: '"single" | "multi"',
2115 description: "Auto-prepend a checkbox column. `multi` puts a select-all checkbox in the header; `single` enforces at-most-one selected."
2116 }
2117 ],
2118 events: [
2119 { name: "wpd-table-filter-change", description: "Filter input changed." },
2120 { name: "wpd-table-sort-change", description: "Header click cycled the sort." },
2121 { name: "wpd-table-selection-change", description: "Selection set changed." },
2122 { name: "wpd-table-row-click", description: "Body row clicked (skips data-noclick descendants)." },
2123 { name: "wpd-table-expand-change", description: "Sub-table toggled." }
2124 ],
2125 slots: [
2126 { name: "empty", description: "Custom empty-state content (CTA, illustration, etc.)." }
2127 ],
2128 cssProps: [
2129 { name: "--wpd-table-bg" },
2130 { name: "--wpd-table-border" },
2131 { name: "--wpd-table-column-border" },
2132 { name: "--wpd-table-header-bg" },
2133 { name: "--wpd-table-row-hover" },
2134 { name: "--wpd-table-stripe" },
2135 { name: "--wpd-table-cell-padding" },
2136 { name: "--wpd-table-font-size" },
2137 { name: "--wpd-table-max-height" },
2138 { name: "--wpd-table-skeleton-color" }
2139 ],
2140 example: html`
2141 <wpd-table id="sample-table" sticky-header striped hover></wpd-table>
2142 `
2143 };
2144 let WpdTable = _WpdTable;
2145 function isTemplateResult(v) {
2146 return !!v && v.__wpdHtml === true;
2147 }
2148 function compareValues(a, b) {
2149 if (a === b) {
2150 return 0;
2151 }
2152 if (a === null || a === void 0) {
2153 return -1;
2154 }
2155 if (b === null || b === void 0) {
2156 return 1;
2157 }
2158 if (typeof a === "number" && typeof b === "number") {
2159 return a - b;
2160 }
2161 if (a instanceof Date && b instanceof Date) {
2162 return a.getTime() - b.getTime();
2163 }
2164 const an = Number(a);
2165 const bn = Number(b);
2166 if (Number.isFinite(an) && Number.isFinite(bn)) {
2167 return an - bn;
2168 }
2169 return String(a).localeCompare(String(b));
2170 }
2171 defineComponent("wpd-table", WpdTable);
2172 const styles$b = css`:host{display:flex;flex-direction:column;gap:var( --wpd-card-gap,12px );padding:var( --wpd-card-padding,16px );border:1px solid var( --wpd-card-border,var( --wpd-border,rgba( 0,0,0,0.08 ) ) );border-radius:var( --wpd-card-radius,12px );background:var( --wpd-card-bg,#fff );color:var( --wpd-card-fg,inherit );box-sizing:border-box;min-width:0;transition:transform 180ms ease,box-shadow 180ms ease,border-color 180ms ease}:host( [ hidden ] ){display:none}:host( [ compact ] ){padding:var( --wpd-card-padding-compact,10px );gap:var( --wpd-card-gap-compact,6px );border-radius:var( --wpd-card-radius-compact,8px )}:host( [ interactive ] ){cursor:pointer;outline-offset:2px}:host( [ interactive ]:hover ),:host( [ interactive ]:focus-visible ){transform:translateY( -2px );box-shadow:var( --wpd-card-shadow-hover,0 4px 16px rgba( 0,0,0,0.08 ) );border-color:var( --wpd-card-border-hover,var( --wpd-border-strong,rgba( 0,0,0,0.16 ) ) )}:host( [ selected ] ){border-color:var( --wpd-card-border-selected,var( --wp-admin-theme-color,#2271b1 ) );box-shadow:var( --wpd-card-shadow-selected,0 0 0 1px var( --wp-admin-theme-color,#2271b1 ) inset )}:host( [ disabled ] ){opacity:0.55;pointer-events:none;cursor:not-allowed}@media ( prefers-reduced-motion:reduce ){:host{transition:none}:host( [ interactive ]:hover ),:host( [ interactive ]:focus-visible ){transform:none}}::slotted( header ){display:flex;align-items:center;gap:12px;min-width:0}::slotted( footer ){display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto}`;
2173 const NOCLICK_SELECTOR = "[data-noclick]";
2174 const _WpdCard = class _WpdCard extends Component {
2175 constructor() {
2176 super(...arguments);
2177 this._onClick = (ev) => {
2178 if (!this.hasAttribute("interactive") || this.hasAttribute("disabled")) {
2179 return;
2180 }
2181 const target = ev.target;
2182 if (target?.closest(NOCLICK_SELECTOR)) {
2183 return;
2184 }
2185 this._emitCardClick(ev);
2186 };
2187 this._onKeyDown = (ev) => {
2188 if (!this.hasAttribute("interactive") || this.hasAttribute("disabled")) {
2189 return;
2190 }
2191 if (ev.key !== "Enter" && ev.key !== " " && ev.key !== "Spacebar") {
2192 return;
2193 }
2194 const target = ev.target;
2195 if (target && target !== this && target.closest(NOCLICK_SELECTOR)) {
2196 return;
2197 }
2198 if (target instanceof HTMLElement && target !== this && (target.tagName === "BUTTON" || target.tagName === "A" || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT")) {
2199 return;
2200 }
2201 ev.preventDefault();
2202 this._emitCardClick(ev);
2203 };
2204 }
2205 connectedCallback() {
2206 super.connectedCallback();
2207 this._syncRoles();
2208 this.addEventListener("click", this._onClick);
2209 this.addEventListener("keydown", this._onKeyDown);
2210 }
2211 disconnectedCallback() {
2212 this.removeEventListener("click", this._onClick);
2213 this.removeEventListener("keydown", this._onKeyDown);
2214 }
2215 render() {
2216 this._syncRoles();
2217 return html`<slot name="header"></slot><slot></slot><slot name="footer"></slot>`;
2218 }
2219 _syncRoles() {
2220 const interactive = this.hasAttribute("interactive");
2221 const disabled = this.hasAttribute("disabled");
2222 if (interactive) {
2223 if (!this.hasAttribute("role")) {
2224 this.setAttribute("role", "button");
2225 }
2226 this.setAttribute("tabindex", disabled ? "-1" : "0");
2227 this.setAttribute("aria-disabled", disabled ? "true" : "false");
2228 } else {
2229 this.removeAttribute("role");
2230 this.removeAttribute("tabindex");
2231 this.removeAttribute("aria-disabled");
2232 }
2233 }
2234 _emitCardClick(originalEvent) {
2235 this.dispatchEvent(
2236 new CustomEvent("wpd-card-click", {
2237 detail: { originalEvent },
2238 bubbles: true,
2239 composed: true
2240 })
2241 );
2242 }
2243 };
2244 _WpdCard.props = [
2245 "interactive",
2246 "selected",
2247 "compact",
2248 "disabled"
2249 ];
2250 _WpdCard.styles = [styles$b];
2251 _WpdCard.help = {
2252 title: "Card",
2253 summary: "Generic hover-aware container. Becomes click-emitting + focusable when `interactive`. Slots for header / default body / footer have built-in layout rhythm so consumers don't need bespoke wrapper components.",
2254 status: "stable",
2255 since: "0.9.0",
2256 props: [
2257 {
2258 name: "interactive",
2259 type: "boolean",
2260 default: "false",
2261 description: 'Surfaces hover lift + cursor + role="button" + emits `wpd-card-click` on click / Enter / Space.'
2262 },
2263 {
2264 name: "selected",
2265 type: "boolean",
2266 default: "false",
2267 description: "Paints the accent ring — pickers / single-selection lists turn this on for the active card."
2268 },
2269 {
2270 name: "compact",
2271 type: "boolean",
2272 default: "false",
2273 description: "Tighter padding + smaller radius for dense lists."
2274 },
2275 {
2276 name: "disabled",
2277 type: "boolean",
2278 default: "false",
2279 description: "Fades the card and blocks pointer / key input. No `wpd-card-click` while disabled."
2280 }
2281 ],
2282 events: [
2283 {
2284 name: "wpd-card-click",
2285 detail: "{ originalEvent: MouseEvent | KeyboardEvent }",
2286 description: "Fires when an interactive card is activated. Skips events whose target is inside a `[data-noclick]` descendant so inline action buttons don't double-fire."
2287 }
2288 ],
2289 slots: [
2290 {
2291 name: "(default)",
2292 description: "Card body. Free-form content."
2293 },
2294 {
2295 name: "header",
2296 description: "Top slot — laid out as a flex row with 12px gap. Drop an `<img>` / icon plus a title block."
2297 },
2298 {
2299 name: "footer",
2300 description: "Bottom slot — pinned via `margin-top: auto`. Standard pattern: meta on the left, primary CTA on the right."
2301 }
2302 ],
2303 cssProps: [
2304 { name: "--wpd-card-bg", default: "#fff" },
2305 { name: "--wpd-card-fg", default: "inherit" },
2306 { name: "--wpd-card-padding", default: "16px" },
2307 { name: "--wpd-card-padding-compact", default: "10px" },
2308 { name: "--wpd-card-gap", default: "12px" },
2309 { name: "--wpd-card-gap-compact", default: "6px" },
2310 { name: "--wpd-card-radius", default: "12px" },
2311 { name: "--wpd-card-radius-compact", default: "8px" },
2312 { name: "--wpd-card-border", default: "var(--wpd-border, rgba(0,0,0,0.08))" },
2313 { name: "--wpd-card-border-hover", default: "var(--wpd-border-strong, rgba(0,0,0,0.16))" },
2314 { name: "--wpd-card-border-selected", default: "var(--wp-admin-theme-color, #2271b1)" },
2315 { name: "--wpd-card-shadow-hover", default: "0 4px 16px rgba(0,0,0,0.08)" }
2316 ],
2317 example: html`
2318 <wpd-card interactive>
2319 <header>
2320 <wpd-icon name="dashicons-admin-plugins" size="40"></wpd-icon>
2321 <div>
2322 <h3>Akismet</h3>
2323 <p>by Automattic</p>
2324 </div>
2325 </header>
2326 <p>The anti-spam service for WordPress sites.</p>
2327 <footer>
2328 <span>1M+ active</span>
2329 <wpd-button variant="primary" data-noclick>Install</wpd-button>
2330 </footer>
2331 </wpd-card>
2332 `
2333 };
2334 let WpdCard = _WpdCard;
2335 defineComponent("wpd-card", WpdCard);
2336 const styles$a = css`:host{display:inline-flex;align-items:center;gap:var( --wpd-badge-gap,6px );padding:var( --wpd-badge-padding,2px 8px );font:var( --wpd-badge-font,500 12px/1.4 var( --desktop-mode-font,system-ui ) );color:var( --wpd-badge-color,var( --desktop-mode-text,#1d2327 ) );background:var( --wpd-badge-bg,rgba( 0,0,0,0.06 ) );border:var( --wpd-badge-border,1px solid transparent );border-radius:var( --wpd-badge-border-radius,999px );white-space:nowrap;vertical-align:baseline}:host( [ hidden ] ){display:none}.dot{width:var( --wpd-badge-dot-size,8px );height:var( --wpd-badge-dot-size,8px );border-radius:50%;background:currentColor;flex:0 0 auto}:host( [ tone="success" ] ){--wpd-badge-color:var( --wpd-badge-success,#1a7f37 );--wpd-badge-bg:var( --wpd-badge-success-bg,rgba( 26,127,55,0.12 ) )}:host( [ tone="warning" ] ){--wpd-badge-color:var( --wpd-badge-warning,#9a6700 );--wpd-badge-bg:var( --wpd-badge-warning-bg,rgba( 154,103,0,0.12 ) )}:host( [ tone="danger" ] ){--wpd-badge-color:var( --wpd-badge-danger,#cf222e );--wpd-badge-bg:var( --wpd-badge-danger-bg,rgba( 207,34,46,0.12 ) )}:host( [ tone="info" ] ){--wpd-badge-color:var( --wpd-badge-info,#0969da );--wpd-badge-bg:var( --wpd-badge-info-bg,rgba( 9,105,218,0.12 ) )}:host( [ tone="neutral" ] ){--wpd-badge-color:var( --wpd-badge-neutral,#57606a );--wpd-badge-bg:var( --wpd-badge-neutral-bg,rgba( 87,96,106,0.12 ) )}:host( [ no-dot ] ) .dot{display:none}`;
2337 const _WpdBadge = class _WpdBadge extends Component {
2338 render() {
2339 return html`<span class="dot" aria-hidden="true"></span><slot></slot>`;
2340 }
2341 };
2342 _WpdBadge.props = ["tone", "noDot"];
2343 _WpdBadge.styles = [styles$a];
2344 _WpdBadge.help = {
2345 title: "Badge",
2346 summary: "Status pill with a colored leading dot and a label slot. Use for window-attached states, count chips, version markers, and any other small status surface where a leading tone-coded dot communicates meaning at a glance.",
2347 status: "experimental",
2348 since: "0.6.0",
2349 props: [
2350 {
2351 name: "tone",
2352 type: '"success" | "warning" | "danger" | "info" | "neutral"',
2353 description: 'Color tone applied to the dot + pill background. Default is "neutral".'
2354 },
2355 {
2356 name: "no-dot",
2357 type: "boolean",
2358 description: "Suppress the leading dot. Useful for count badges where the label itself is the whole signal."
2359 }
2360 ],
2361 slots: [{ name: "(default)", description: "Badge label." }],
2362 cssProps: [
2363 { name: "--wpd-badge-color", description: "Foreground color (also dot color)." },
2364 { name: "--wpd-badge-bg", description: "Pill background color." },
2365 { name: "--wpd-badge-border", default: "1px solid transparent" },
2366 { name: "--wpd-badge-padding", default: "2px 8px" },
2367 { name: "--wpd-badge-gap", default: "6px" },
2368 { name: "--wpd-badge-dot-size", default: "8px" },
2369 { name: "--wpd-badge-border-radius", default: "999px" }
2370 ],
2371 example: html`
2372 <wpd-badge tone="success">Attached</wpd-badge>
2373 <wpd-badge tone="warning">Detaching…</wpd-badge>
2374 <wpd-badge tone="danger">Errored</wpd-badge>
2375 <wpd-badge tone="info" no-dot>v0.6.0</wpd-badge>
2376 `
2377 };
2378 let WpdBadge = _WpdBadge;
2379 defineComponent("wpd-badge", WpdBadge);
2380 const flyoutStyles = css`:host{display:block;position:absolute;z-index:10;background:var( --wpd-flyout-bg,var( --wpd-surface-elevated,#ffffff ) );color:var( --wpd-flyout-fg,var( --desktop-mode-fg,#1d2327 ) );border-radius:14px;box-shadow:var( --wpd-flyout-shadow,0 16px 48px rgba( 0,25,53,0.4 ) );transform:translateX( 110% );opacity:0;pointer-events:none;transition:transform 220ms cubic-bezier( 0.22,1,0.36,1 ),opacity 180ms ease}:host( [ open ] ){transform:translateX( 0 );opacity:1;pointer-events:auto}:host( [ placement='end' ] ),:host(:not( [ placement ] ) ){inset-block:64px 14px;inset-inline-end:14px;width:min( 320px,calc( 100% - 28px ) )}:host( [ placement='start' ] ){inset-block:64px 14px;inset-inline-start:14px;width:min( 320px,calc( 100% - 28px ) );transform:translateX( -110% )}:host( [ placement='start' ][ open ] ){transform:translateX( 0 )}:host( [ placement='top' ] ){inset-block-start:14px;inset-inline:14px;max-block-size:calc( 100% - 28px );transform:translateY( -110% )}:host( [ placement='top' ][ open ] ){transform:translateY( 0 )}:host-context( [ dir='rtl' ] ):host( [ placement='end' ] ),:host-context( [ dir='rtl' ] ):host(:not( [ placement ] ) ){transform:translateX( -110% )}:host-context( [ dir='rtl' ] ):host( [ placement='end' ][ open ] ),:host-context( [ dir='rtl' ] ):host(:not( [ placement ] ) [ open ] ){transform:translateX( 0 )}:host-context( [ dir='rtl' ] ):host( [ placement='start' ] ){transform:translateX( 110% )}:host-context( [ dir='rtl' ] ):host( [ placement='start' ][ open ] ){transform:translateX( 0 )}:host::before{content:'';position:fixed;inset:0;background:var( --wpd-flyout-backdrop,transparent );z-index:-1;pointer-events:none;transition:opacity 180ms ease;opacity:0}:host( [ open ] )::before{opacity:1}@media ( prefers-reduced-motion:reduce ){:host{transition:none}:host::before{transition:none}}`;
2381 const FOCUSABLE_SELECTOR = [
2382 "a[href]",
2383 "area[href]",
2384 "button:not([disabled])",
2385 'input:not([disabled]):not([type="hidden"])',
2386 "select:not([disabled])",
2387 "textarea:not([disabled])",
2388 '[tabindex]:not([tabindex="-1"])',
2389 '[contenteditable="true"]'
2390 ].join(",");
2391 const CLOSE_BUTTON_SELECTOR = "[data-flyout-close]";
2392 const _WpdFlyout = class _WpdFlyout extends Component {
2393 constructor() {
2394 super(...arguments);
2395 this._trigger = null;
2396 this._onDocKey = null;
2397 this._onScopePointerDown = null;
2398 this._onHostClick = null;
2399 this._onHostKeyDown = null;
2400 this._pendingReason = null;
2401 }
2402 connectedCallback() {
2403 super.connectedCallback();
2404 if (!this.hasAttribute("role")) {
2405 this.setAttribute("role", "dialog");
2406 }
2407 if (!this.hasAttribute("open")) {
2408 this.setAttribute("inert", "");
2409 }
2410 }
2411 disconnectedCallback() {
2412 this._detachOpenListeners();
2413 }
2414 attributeChangedCallback(name, oldValue, newValue) {
2415 super.attributeChangedCallback(name, oldValue, newValue);
2416 if (name === "open") {
2417 if (newValue !== null && oldValue === null) {
2418 this._handleOpen();
2419 } else if (newValue === null && oldValue !== null) {
2420 this._handleClose();
2421 }
2422 }
2423 }
2424 _handleOpen() {
2425 this.removeAttribute("inert");
2426 const doc = this.ownerDocument;
2427 const active = doc?.activeElement ?? null;
2428 this._trigger = active instanceof HTMLElement && active !== this && active !== doc?.body && active !== doc?.documentElement ? active : null;
2429 queueMicrotask(() => {
2430 if (!this.hasAttribute("open")) {
2431 return;
2432 }
2433 const focusable = this._firstFocusable();
2434 (focusable ?? this).focus?.({ preventScroll: true });
2435 });
2436 this._attachOpenListeners();
2437 }
2438 _handleClose() {
2439 const reason = this._pendingReason ?? "api";
2440 this._pendingReason = null;
2441 this.setAttribute("inert", "");
2442 this._detachOpenListeners();
2443 this.emit("wpd-flyout-dismiss", { reason });
2444 const trigger = this._trigger;
2445 this._trigger = null;
2446 if (trigger && trigger.isConnected) {
2447 trigger.focus?.({ preventScroll: true });
2448 }
2449 }
2450 /** Internal dismissal — flags the reason then removes `open`. */
2451 _dismiss(reason) {
2452 if (!this.hasAttribute("open")) {
2453 return;
2454 }
2455 this._pendingReason = reason;
2456 this.removeAttribute("open");
2457 }
2458 _attachOpenListeners() {
2459 const scopeRoot = this._resolveScopeRoot();
2460 this._onScopePointerDown = (e) => {
2461 if (!this.hasAttribute("open")) {
2462 return;
2463 }
2464 const path = e.composedPath();
2465 if (path.includes(this)) {
2466 return;
2467 }
2468 if (this._trigger && path.includes(this._trigger)) {
2469 return;
2470 }
2471 this._dismiss("pointer");
2472 };
2473 scopeRoot.addEventListener("pointerdown", this._onScopePointerDown);
2474 this._onDocKey = (e) => {
2475 if (e.key === "Escape" && this.hasAttribute("open")) {
2476 e.preventDefault();
2477 this._dismiss("escape");
2478 }
2479 };
2480 document.addEventListener("keydown", this._onDocKey);
2481 this._onHostKeyDown = (e) => {
2482 if (e.key !== "Tab") {
2483 return;
2484 }
2485 const focusables = this._allFocusable();
2486 if (focusables.length === 0) {
2487 e.preventDefault();
2488 return;
2489 }
2490 const first = focusables[0];
2491 const last = focusables[focusables.length - 1];
2492 const active = this.ownerDocument?.activeElement ?? null;
2493 if (e.shiftKey && (active === first || active === this)) {
2494 e.preventDefault();
2495 last.focus({ preventScroll: true });
2496 } else if (!e.shiftKey && active === last) {
2497 e.preventDefault();
2498 first.focus({ preventScroll: true });
2499 }
2500 };
2501 this.addEventListener("keydown", this._onHostKeyDown);
2502 this._onHostClick = (e) => {
2503 const target = e.target;
2504 const closeBtn = target?.closest?.(CLOSE_BUTTON_SELECTOR);
2505 if (closeBtn && this.contains(closeBtn)) {
2506 this._dismiss("close-button");
2507 }
2508 };
2509 this.addEventListener("click", this._onHostClick);
2510 }
2511 _detachOpenListeners() {
2512 if (this._onDocKey) {
2513 document.removeEventListener("keydown", this._onDocKey);
2514 this._onDocKey = null;
2515 }
2516 if (this._onScopePointerDown) {
2517 const scopeRoot = this._resolveScopeRoot();
2518 scopeRoot.removeEventListener("pointerdown", this._onScopePointerDown);
2519 this._onScopePointerDown = null;
2520 }
2521 if (this._onHostKeyDown) {
2522 this.removeEventListener("keydown", this._onHostKeyDown);
2523 this._onHostKeyDown = null;
2524 }
2525 if (this._onHostClick) {
2526 this.removeEventListener("click", this._onHostClick);
2527 this._onHostClick = null;
2528 }
2529 }
2530 _resolveScopeRoot() {
2531 const scope = this.getAttribute("scope") ?? "window";
2532 if (scope === "document") {
2533 return document.body;
2534 }
2535 if (scope === "parent") {
2536 return this.parentElement ?? document.body;
2537 }
2538 const windowBody = this.closest(".desktop-mode-window__body");
2539 return windowBody ?? this.parentElement ?? document.body;
2540 }
2541 _firstFocusable() {
2542 const slotMatch = this.querySelector(FOCUSABLE_SELECTOR);
2543 return slotMatch ?? null;
2544 }
2545 _allFocusable() {
2546 return Array.from(
2547 this.querySelectorAll(FOCUSABLE_SELECTOR)
2548 ).filter((el) => !el.disabled);
2549 }
2550 render() {
2551 return html`<slot></slot>`;
2552 }
2553 };
2554 _WpdFlyout.props = [
2555 "open",
2556 "placement",
2557 "scope",
2558 "aria-label",
2559 "aria-labelledby"
2560 ];
2561 _WpdFlyout.styles = [flyoutStyles];
2562 _WpdFlyout.help = {
2563 title: "Flyout",
2564 summary: "Window-scoped sliding card. Lives `position: absolute` inside a window body, slides in from the configured edge with margins on every side, captures the previously-focused element as the trigger for restore-on-close, traps focus while open, and dismisses on Escape / pointerdown-outside / `[data-flyout-close]` click / imperative `open`-removal — all firing one `wpd-flyout-dismiss` event with a `reason` discriminator.",
2565 status: "experimental",
2566 since: "0.8.2",
2567 props: [
2568 {
2569 name: "open",
2570 type: "boolean attribute",
2571 description: "Mounts the flyout open. Removing the attribute (programmatically or via the component's own dismissal paths) slides it back out and fires `wpd-flyout-dismiss`."
2572 },
2573 {
2574 name: "placement",
2575 type: "'end' | 'start' | 'top'",
2576 default: "end",
2577 description: "Which inside-window edge the card anchors to. `'end'` is the inline-end edge (right in LTR, left in RTL). All placements keep gutters on every edge — the panel reads as a floating card, not a drawer."
2578 },
2579 {
2580 name: "scope",
2581 type: "'window' | 'parent' | 'document'",
2582 default: "window",
2583 description: "Which container the click-outside listener attaches to. `'window'` (default) walks up to the closest `.desktop-mode-window__body`; `'parent'` uses the immediate parent element; `'document'` listens on `document.body`. The first option is the right one for any flyout inside a desktop-mode window."
2584 },
2585 {
2586 name: "aria-label",
2587 type: "string",
2588 description: "Accessible name for the dialog landmark."
2589 },
2590 {
2591 name: "aria-labelledby",
2592 type: "id reference",
2593 description: "Id of the element labelling the flyout — wins over `aria-label` when both are set."
2594 }
2595 ],
2596 events: [
2597 {
2598 name: "wpd-flyout-dismiss",
2599 description: "Fires whenever the flyout closes. Detail: `{ reason: 'escape' | 'pointer' | 'close-button' | 'api' }`. The `'api'` reason fires when an external caller imperatively removes the `open` attribute."
2600 }
2601 ],
2602 cssProps: [
2603 {
2604 name: "--wpd-flyout-bg",
2605 description: "Card background. Default: white surface."
2606 },
2607 {
2608 name: "--wpd-flyout-fg",
2609 description: "Card foreground. Default: `--desktop-mode-fg`."
2610 },
2611 {
2612 name: "--wpd-flyout-shadow",
2613 description: "Drop shadow. Default: a deep navy 16px/48px lift."
2614 },
2615 {
2616 name: "--wpd-flyout-backdrop",
2617 description: "Backdrop layer dimming the window body while the flyout is open. Default `transparent` — flyout is additive, not modal. Set to e.g. `rgba(0,0,0,0.4)` for window-scoped modality."
2618 }
2619 ],
2620 slots: [
2621 {
2622 name: "(default)",
2623 description: "Card content. The component does not impose padding or a header — wrap in your own `<wpd-panel>` / header / scroll container as needed. Mark a button with `data-flyout-close` to wire it to the framework dismissal path."
2624 }
2625 ],
2626 example: html`
2627 <div
2628 style="position:relative;height:280px;border:1px solid rgba(0,0,0,0.08);border-radius:8px;background:var(--desktop-mode-bg-soft, #f6f7f7);overflow:hidden;"
2629 >
2630 <div
2631 style="height:32px;background:rgba(0,0,0,0.04);display:flex;align-items:center;padding:0 12px;font-size:12px;opacity:0.7;"
2632 >
2633 Mock window — title bar
2634 </div>
2635 <div style="padding:12px;">
2636 <wpd-button
2637 id="wpd-flyout-example-trigger"
2638 @click=${(e) => {
2639 const flyout = e.currentTarget.getRootNode().querySelector("#wpd-flyout-example-flyout");
2640 flyout?.setAttribute("open", "");
2641 }}
2642 >Open flyout</wpd-button
2643 >
2644 <p style="opacity:0.7;font-size:13px;">
2645 Click the button. The card slides in from the right
2646 edge with margins from every window edge — title
2647 bar stays visible above. Press Escape, click outside
2648 the card, or hit Close to dismiss.
2649 </p>
2650 </div>
2651 <wpd-flyout
2652 id="wpd-flyout-example-flyout"
2653 placement="end"
2654 scope="parent"
2655 aria-label="Sample flyout"
2656 >
2657 <div style="padding:18px;">
2658 <h4 style="margin:0 0 8px;">Account</h4>
2659 <p style="margin:0 0 12px;font-size:13px;opacity:0.8;">
2660 Floating card inside the window. Margins from
2661 every edge so the chrome reads through.
2662 </p>
2663 <wpd-button data-flyout-close>Close</wpd-button>
2664 </div>
2665 </wpd-flyout>
2666 </div>
2667 `
2668 };
2669 let WpdFlyout = _WpdFlyout;
2670 defineComponent("wpd-flyout", WpdFlyout);
2671 function getWpHooks() {
2672 const hooks = window.wp?.hooks;
2673 if (!hooks) {
2674 throw new Error(
2675 "[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."
2676 );
2677 }
2678 return hooks;
2679 }
2680 function addAction(hookName2, namespace, callback, priority) {
2681 getWpHooks().addAction(
2682 hookName2,
2683 namespace,
2684 callback,
2685 priority
2686 );
2687 }
2688 function removeAction(hookName2, namespace) {
2689 return getWpHooks().removeAction(hookName2, namespace);
2690 }
2691 function applyFilters(hookName2, value, ...args) {
2692 return getWpHooks().applyFilters(hookName2, value, ...args);
2693 }
2694 function doAction(hookName2, ...args) {
2695 getWpHooks().doAction(hookName2, ...args);
2696 }
2697 const HOOKS = {
2698 /**
2699 * Action, fires when one of the shell's own try/catch barriers
2700 * catches an exception. Payload: `{ scope:
2701 * 'widget-mount' | 'widget-teardown' | 'window-open' | 'wallpaper-mount' |
2702 * 'wallpaper-teardown' | 'session-save' | 'menu-refresh' | string,
2703 * id?: string, error: unknown }`. Paired with the existing
2704 * `console.error` calls — a monitor widget can surface these as
2705 * first-class entries.
2706 */
2707 SHELL_ERROR: "desktop-mode.shell.error",
2708 /**
2709 * Action, fires once per `wp.desktop.broadcast()` call with the
2710 * fully-resolved `{ topic, payload }` detail. Lets plugins log,
2711 * mirror, or augment broadcast traffic without subscribing for
2712 * every individual topic.
2713 */
2714 BROADCAST: "desktop-mode.broadcast"
2715 };
2716 const HOOK_PREFIX = "desktop-mode.activity.";
2717 function hookName(channel) {
2718 return `${HOOK_PREFIX}${String(channel)}`;
2719 }
2720 let subscribeSeq = 0;
2721 const activity = {
2722 publish(channel, payload) {
2723 doAction(hookName(channel), payload);
2724 },
2725 subscribe(channel, cb) {
2726 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
2727 const hook = hookName(channel);
2728 addAction(
2729 hook,
2730 ns,
2731 (payload) => cb(payload)
2732 );
2733 let removed = false;
2734 return () => {
2735 if (removed) {
2736 return;
2737 }
2738 removed = true;
2739 removeAction(hook, ns);
2740 };
2741 },
2742 filter(channel, value, ...args) {
2743 return applyFilters(hookName(channel), value, ...args);
2744 }
2745 };
2746 const EVENT_NAME = "desktop-mode-broadcast";
2747 function broadcast(topic, payload) {
2748 const filteredTopic = String(
2749 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
2750 );
2751 const filteredPayload = applyFilters(
2752 "desktop-mode.broadcast.payload",
2753 payload,
2754 { topic: filteredTopic }
2755 );
2756 const detail = {
2757 topic: filteredTopic,
2758 payload: filteredPayload
2759 };
2760 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
2761 doAction(HOOKS.BROADCAST, detail);
2762 activity.publish(
2763 filteredTopic,
2764 filteredPayload
2765 );
2766 {
2767 return;
2768 }
2769 }
2770 function subscribe(topic, cb) {
2771 const handler = (e) => {
2772 const detail = e.detail;
2773 if (!detail) {
2774 return;
2775 }
2776 if (detail.topic !== topic) {
2777 return;
2778 }
2779 try {
2780 cb(detail.payload, { topic: detail.topic });
2781 } catch (err) {
2782 doAction(HOOKS.SHELL_ERROR, {
2783 scope: "broadcast-subscriber",
2784 topic: detail.topic,
2785 error: err
2786 });
2787 }
2788 };
2789 document.addEventListener(EVENT_NAME, handler);
2790 return () => document.removeEventListener(EVENT_NAME, handler);
2791 }
2792 const styles$9 = css`:host{display:inline-flex}:host( [ fill-cell ] ){display:flex;width:100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:var( --wpd-button-padding,6px 12px );border-radius:var( --wpd-button-border-radius,6px );font:inherit;font-weight:500;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease;background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid var( --desktop-mode-border,#c3c4c7 ) )}:host( [ fill-cell ] ) button{width:100%;min-height:var( --wpd-button-min-height,44px )}button:disabled{opacity:0.5;cursor:not-allowed}button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.04 ) )}:host( [ variant='primary' ] ) button{background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) );color:var( --wpd-button-fg,#fff );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='primary' ] ) button:hover:not(:disabled ){filter:brightness( 1.06 );background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ variant='secondary' ] ) button{background:var( --wpd-button-bg,rgba( 0,0,0,0.06 ) );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='secondary' ] ) button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.1 ) )}:host( [ variant='danger' ] ) button{background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,#d63638 );border:var( --wpd-button-border,1px solid currentColor )}:host( [ variant='danger' ] ) button:hover:not(:disabled ){background:#d63638;color:#fff}:host( [ variant='link' ] ) button{background:transparent;color:var( --wpd-button-fg,var( --wp-admin-theme-color,#2271b1 ) );border:0;padding:0;text-decoration:underline}:host( [ busy ] ) button{pointer-events:none;opacity:0.75}.wpd-button__spinner{box-sizing:border-box;display:inline-block;width:12px;height:12px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:wpd-button-spin 0.6s linear infinite;flex-shrink:0}@keyframes wpd-button-spin{to{transform:rotate( 360deg )}}`;
2793 const _WpdButton = class _WpdButton extends Component {
2794 render() {
2795 const disabled = this.disabled !== null;
2796 const busy = this.busy !== null;
2797 const type = this.type || "button";
2798 return html`
2799 <button
2800 part="button"
2801 type=${type}
2802 ?disabled=${disabled || busy}
2803 aria-busy=${busy ? "true" : "false"}
2804 >
2805 ${busy ? html`<span class="wpd-button__spinner" aria-hidden="true"></span>` : ""}
2806 <slot></slot>
2807 </button>
2808 `;
2809 }
2810 };
2811 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
2812 _WpdButton.styles = [styles$9];
2813 _WpdButton.help = {
2814 title: "Button",
2815 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
2816 status: "stable",
2817 since: "0.9.0",
2818 props: [
2819 {
2820 name: "variant",
2821 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
2822 default: "ghost",
2823 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
2824 },
2825 {
2826 name: "disabled",
2827 type: "boolean attribute",
2828 description: "Disable pointer + keyboard interaction and dim the chrome."
2829 },
2830 {
2831 name: "type",
2832 type: "'button' | 'submit' | 'reset'",
2833 default: "button",
2834 description: "Forwarded to the underlying native <button>."
2835 },
2836 {
2837 name: "busy",
2838 type: "boolean attribute",
2839 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
2840 },
2841 {
2842 name: "fill-cell",
2843 type: "boolean attribute",
2844 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
2845 }
2846 ],
2847 slots: [{ name: "(default)", description: "Button label." }],
2848 parts: [{ name: "button", description: "Underlying <button> element." }],
2849 cssProps: [
2850 { name: "--wpd-button-bg", description: "Background color." },
2851 {
2852 name: "--wpd-button-bg-hover",
2853 description: "Hover wash (ghost + secondary variants)."
2854 },
2855 { name: "--wpd-button-fg", description: "Text color." },
2856 { name: "--wpd-button-border", description: "Border shorthand." },
2857 { name: "--wpd-button-border-radius", default: "6px" },
2858 { name: "--wpd-button-padding", default: "6px 12px" },
2859 {
2860 name: "--wpd-button-min-height",
2861 description: "Minimum height when fill-cell is set."
2862 }
2863 ],
2864 example: html`
2865 <wpd-cluster gap="8">
2866 <wpd-button variant="primary">Primary</wpd-button>
2867 <wpd-button variant="secondary">Secondary</wpd-button>
2868 <wpd-button variant="ghost">Ghost</wpd-button>
2869 <wpd-button variant="danger">Danger</wpd-button>
2870 <wpd-button variant="link">Link</wpd-button>
2871 </wpd-cluster>
2872 `
2873 };
2874 let WpdButton = _WpdButton;
2875 defineComponent("wpd-button", WpdButton);
2876 function buildCard(plugin, installed, callbacks) {
2877 const card = document.createElement("wpd-card");
2878 card.classList.add("desktop-mode-plugins__card");
2879 card.setAttribute("interactive", "");
2880 card.dataset.slug = plugin.slug;
2881 card.setAttribute(
2882 "aria-label",
2883 sprintf(
2884 /* translators: %s: plugin name */
2885 __("View details for %s", "desktop-mode"),
2886 plugin.name
2887 )
2888 );
2889 const header = document.createElement("header");
2890 header.className = "desktop-mode-plugins__card-header";
2891 const iconWrap = document.createElement("div");
2892 iconWrap.className = "desktop-mode-plugins__card-icon";
2893 const iconUrl = pickIcon(plugin.icons);
2894 if (iconUrl) {
2895 const img = document.createElement("img");
2896 img.src = iconUrl;
2897 img.alt = "";
2898 img.loading = "lazy";
2899 img.decoding = "async";
2900 img.addEventListener(
2901 "load",
2902 () => img.classList.add("is-loaded")
2903 );
2904 img.addEventListener("error", () => {
2905 iconWrap.replaceChildren(buildFallbackGlyph$1());
2906 });
2907 iconWrap.appendChild(img);
2908 } else {
2909 iconWrap.appendChild(buildFallbackGlyph$1());
2910 }
2911 const titleBlock = document.createElement("div");
2912 titleBlock.className = "desktop-mode-plugins__card-titleblock";
2913 const title = document.createElement("h3");
2914 title.className = "desktop-mode-plugins__card-title";
2915 title.textContent = decodeEntities(plugin.name);
2916 const byline = document.createElement("p");
2917 byline.className = "desktop-mode-plugins__card-byline";
2918 byline.innerHTML = sprintf(
2919 /* translators: %s: plugin author name (HTML-stripped) */
2920 __("by %s", "desktop-mode"),
2921 `<span>${escapeHtml$2(stripHtml$4(plugin.author ?? ""))}</span>`
2922 );
2923 titleBlock.append(title, byline);
2924 header.setAttribute("slot", "header");
2925 header.append(iconWrap, titleBlock);
2926 const desc = document.createElement("p");
2927 desc.className = "desktop-mode-plugins__card-desc";
2928 desc.textContent = decodeEntities(plugin.short_description ?? "");
2929 const footer = document.createElement("footer");
2930 footer.className = "desktop-mode-plugins__card-footer";
2931 footer.setAttribute("slot", "footer");
2932 const meta = document.createElement("div");
2933 meta.className = "desktop-mode-plugins__card-meta";
2934 meta.appendChild(buildStarCluster(plugin.rating ?? 0, plugin.num_ratings ?? 0));
2935 const installs = document.createElement("span");
2936 installs.className = "desktop-mode-plugins__card-installs";
2937 installs.textContent = formatInstalls(plugin.active_installs ?? 0);
2938 meta.appendChild(installs);
2939 const cta = buildCta(plugin, installed, callbacks, card);
2940 footer.append(meta, cta);
2941 card.append(header, desc, footer);
2942 card.addEventListener("wpd-card-click", () => {
2943 callbacks.onOpen(plugin.slug, plugin);
2944 });
2945 return card;
2946 }
2947 function repaintCardCta(card, plugin, installed, callbacks) {
2948 const footer = card.querySelector(
2949 ".desktop-mode-plugins__card-footer"
2950 );
2951 if (!footer) {
2952 return;
2953 }
2954 const previous = footer.querySelector(
2955 "[data-plugin-card-cta]"
2956 );
2957 if (previous) {
2958 previous.remove();
2959 }
2960 footer.appendChild(buildCta(plugin, installed, callbacks, card));
2961 }
2962 function buildCta(plugin, installed, callbacks, card) {
2963 const installedRow = installed.get(plugin.slug);
2964 const button2 = document.createElement("wpd-button");
2965 button2.setAttribute("data-plugin-card-cta", "");
2966 button2.setAttribute("data-noclick", "");
2967 if (installedRow) {
2968 if (installedRow.status === "active" || installedRow.status === "active-network") {
2969 button2.setAttribute("variant", "ghost");
2970 button2.setAttribute("disabled", "");
2971 button2.textContent = __("Active", "desktop-mode");
2972 } else {
2973 button2.setAttribute("variant", "primary");
2974 button2.textContent = __("Activate", "desktop-mode");
2975 button2.addEventListener("click", (ev) => {
2976 ev.stopPropagation();
2977 void callbacks.onActivate(installedRow, card);
2978 });
2979 }
2980 } else {
2981 button2.setAttribute("variant", "primary");
2982 button2.textContent = __("Install", "desktop-mode");
2983 button2.addEventListener("click", (ev) => {
2984 ev.stopPropagation();
2985 void callbacks.onInstall(plugin, card);
2986 });
2987 }
2988 return button2;
2989 }
2990 function buildStarCluster(rating0to100, totalRatings) {
2991 const wrap = document.createElement("span");
2992 wrap.className = "desktop-mode-plugins__stars";
2993 wrap.setAttribute("aria-label", formatStarsAriaLabel(rating0to100));
2994 const stars5 = Math.max(0, Math.min(5, rating0to100 / 100 * 5));
2995 const full = Math.floor(stars5);
2996 const half = stars5 - full >= 0.5 ? 1 : 0;
2997 const empty = 5 - full - half;
2998 for (let i = 0; i < full; i++) {
2999 wrap.appendChild(buildStar("filled"));
3000 }
3001 for (let i = 0; i < half; i++) {
3002 wrap.appendChild(buildStar("half"));
3003 }
3004 for (let i = 0; i < empty; i++) {
3005 wrap.appendChild(buildStar("empty"));
3006 }
3007 if (totalRatings > 0) {
3008 const count = document.createElement("span");
3009 count.className = "desktop-mode-plugins__stars-count";
3010 count.textContent = `(${formatThousands(totalRatings)})`;
3011 wrap.appendChild(count);
3012 }
3013 return wrap;
3014 }
3015 function buildStar(kind) {
3016 const span = document.createElement("span");
3017 span.className = "desktop-mode-plugins__star";
3018 span.setAttribute("aria-hidden", "true");
3019 const icon = document.createElement("span");
3020 if (kind === "filled") {
3021 icon.className = "dashicons dashicons-star-filled";
3022 } else if (kind === "half") {
3023 icon.className = "dashicons dashicons-star-half";
3024 } else {
3025 icon.className = "dashicons dashicons-star-empty";
3026 }
3027 span.appendChild(icon);
3028 return span;
3029 }
3030 function buildFallbackGlyph$1() {
3031 const fallback = document.createElement("span");
3032 fallback.className = "dashicons dashicons-admin-plugins desktop-mode-plugins__card-icon-fallback";
3033 fallback.setAttribute("aria-hidden", "true");
3034 return fallback;
3035 }
3036 function pickIcon(icons) {
3037 if (!icons) {
3038 return null;
3039 }
3040 return icons.svg ?? icons["256"] ?? icons["256x256"] ?? icons.default ?? icons["128"] ?? icons["128x128"] ?? icons["2x"] ?? icons["1x"] ?? Object.values(icons)[0] ?? null;
3041 }
3042 function formatInstalls(n) {
3043 if (n <= 0) {
3044 return __("Fewer than 10 active", "desktop-mode");
3045 }
3046 if (n >= 1e6) {
3047 const millions = Math.floor(n / 1e6);
3048 return sprintf(
3049 /* translators: %d: integer number of millions of active installs */
3050 __("%d+ million active", "desktop-mode"),
3051 millions
3052 );
3053 }
3054 if (n >= 1e3) {
3055 return sprintf(
3056 /* translators: %s: comma-grouped active install count */
3057 __("%s+ active", "desktop-mode"),
3058 formatThousands(roundTo3SigFigs(n))
3059 );
3060 }
3061 return sprintf(
3062 /* translators: %s: comma-grouped active install count */
3063 __("%s+ active", "desktop-mode"),
3064 formatThousands(n)
3065 );
3066 }
3067 function roundTo3SigFigs(n) {
3068 const order = Math.pow(10, Math.floor(Math.log10(n)) - 2);
3069 return Math.floor(n / order) * order;
3070 }
3071 function formatStarsAriaLabel(rating0to100) {
3072 const stars5 = Math.max(0, Math.min(5, rating0to100 / 100 * 5));
3073 return sprintf(
3074 /* translators: %s: rating out of 5 (one decimal) */
3075 __("Rated %s out of 5", "desktop-mode"),
3076 stars5.toFixed(1)
3077 );
3078 }
3079 function formatThousands(n) {
3080 try {
3081 return new Intl.NumberFormat().format(n);
3082 } catch {
3083 return String(n);
3084 }
3085 }
3086 const _entityCache = document.createElement("textarea");
3087 function decodeEntities(html2) {
3088 if (!html2) {
3089 return "";
3090 }
3091 _entityCache.innerHTML = html2;
3092 return _entityCache.value;
3093 }
3094 function escapeHtml$2(raw) {
3095 const tmp = document.createElement("div");
3096 tmp.textContent = raw;
3097 return tmp.innerHTML;
3098 }
3099 function stripHtml$4(html2) {
3100 const tmp = document.createElement("div");
3101 tmp.innerHTML = html2;
3102 return tmp.textContent ?? "";
3103 }
3104 function api() {
3105 return window.wp?.desktop ?? null;
3106 }
3107 function makeCardDraggable(card, plugin) {
3108 if (card.dataset.dragWired === "1") {
3109 return;
3110 }
3111 card.dataset.dragWired = "1";
3112 card.addEventListener("pointerdown", (ev) => {
3113 const desktop = api();
3114 const manager = desktop?.dragManager;
3115 if (!manager) {
3116 return;
3117 }
3118 const t = ev.target;
3119 if (t?.closest("[data-plugin-card-cta]")) {
3120 return;
3121 }
3122 manager.start({
3123 payload: {
3124 type: "wporg-plugin",
3125 source: card,
3126 data: {
3127 slug: plugin.slug,
3128 name: plugin.name,
3129 iconUrl: pickIcon(plugin.icons) ?? null,
3130 homepage: plugin.homepage ?? "",
3131 authorName: stripHtml$3(plugin.author ?? ""),
3132 shortDescription: plugin.short_description ?? ""
3133 },
3134 ghost: buildGhost(plugin, card, ev)
3135 },
3136 origin: ev,
3137 onClickOnly: () => {
3138 }
3139 });
3140 });
3141 }
3142 function installPluginDropTargets() {
3143 const desktop = api();
3144 const manager = desktop?.dragManager;
3145 if (!manager) {
3146 return () => {
3147 };
3148 }
3149 const teardowns = [];
3150 const dock = findDockElement();
3151 if (dock) {
3152 const off = manager.registerDropTarget({
3153 id: "desktop-mode-plugins-window/dock",
3154 element: dock,
3155 accept: (p) => p.type === "wporg-plugin",
3156 onEnter: () => {
3157 dock.setAttribute("data-plugins-card-drop-active", "");
3158 },
3159 onLeave: () => {
3160 dock.removeAttribute("data-plugins-card-drop-active");
3161 },
3162 onDrop: (session) => {
3163 dock.removeAttribute("data-plugins-card-drop-active");
3164 const data = session.payload.data;
3165 const slug = String(data.slug ?? "");
3166 if (!slug) {
3167 return;
3168 }
3169 const name = String(data.name ?? slug);
3170 const icon = typeof data.iconUrl === "string" && data.iconUrl ? data.iconUrl : "dashicons-admin-plugins";
3171 const homepage = String(data.homepage ?? "");
3172 const url = homepage !== "" ? homepage : `https://wordpress.org/plugins/${encodeURIComponent(slug)}/`;
3173 if (typeof desktop?.registerSystemTile === "function") {
3174 desktop.registerSystemTile({
3175 id: `wporg-plugin-${slug}`,
3176 title: name,
3177 icon,
3178 url
3179 });
3180 }
3181 if (typeof desktop?.showToast === "function") {
3182 desktop.showToast({
3183 message: sprintf(
3184 /* translators: %s: plugin name */
3185 __("Pinned %s to the dock.", "desktop-mode"),
3186 name
3187 ),
3188 duration: 3500
3189 });
3190 }
3191 }
3192 });
3193 teardowns.push(off);
3194 }
3195 return () => {
3196 for (const off of teardowns) {
3197 try {
3198 off();
3199 } catch {
3200 }
3201 }
3202 };
3203 }
3204 function findDockElement() {
3205 return document.querySelector(".desktop-mode-bottom-dock") ?? document.querySelector(".desktop-mode-dock") ?? document.querySelector("[data-desktop-mode-dock]");
3206 }
3207 function buildGhost(plugin, card, origin) {
3208 const rect = card.getBoundingClientRect();
3209 const offsetX = origin.clientX - rect.left;
3210 const offsetY = origin.clientY - rect.top;
3211 const ghost = document.createElement("div");
3212 ghost.className = "desktop-mode-plugins__drag-ghost";
3213 const iconUrl = pickIcon(plugin.icons);
3214 if (iconUrl) {
3215 const img = document.createElement("img");
3216 img.src = iconUrl;
3217 img.alt = "";
3218 ghost.appendChild(img);
3219 } else {
3220 const fallback = document.createElement("span");
3221 fallback.className = "dashicons dashicons-admin-plugins desktop-mode-plugins__drag-ghost-fallback";
3222 ghost.appendChild(fallback);
3223 }
3224 const label = document.createElement("span");
3225 label.textContent = plugin.name;
3226 ghost.appendChild(label);
3227 return { offsetX, offsetY, element: ghost };
3228 }
3229 function stripHtml$3(html2) {
3230 const tmp = document.createElement("div");
3231 tmp.innerHTML = html2;
3232 return tmp.textContent ?? "";
3233 }
3234 const FALLBACK_BASE = "http://localhost/";
3235 function joinRestUrl(restRoot, path) {
3236 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
3237 const url = new URL(restRoot, base);
3238 const trimmed = path.replace(/^\/+/, "");
3239 const queryAt = trimmed.indexOf("?");
3240 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
3241 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
3242 if (url.searchParams.has("rest_route")) {
3243 const existing = url.searchParams.get("rest_route") ?? "/";
3244 const prefix = existing.endsWith("/") ? existing : existing + "/";
3245 url.searchParams.set("rest_route", prefix + route);
3246 } else {
3247 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
3248 url.pathname = pathname + route;
3249 }
3250 if (extraQuery) {
3251 const extras = new URLSearchParams(extraQuery);
3252 extras.forEach((value, key) => {
3253 url.searchParams.append(key, value);
3254 });
3255 }
3256 return url.toString();
3257 }
3258 const WINDOW_ID = "desktop-mode-plugins";
3259 function getConfig() {
3260 const store = window.desktopModeWindowConfig;
3261 const cfg = store ? store[WINDOW_ID] : void 0;
3262 if (!cfg) {
3263 throw new Error(
3264 `[${WINDOW_ID}] config blob is missing — was the window opened without registration? See the matching \`desktop_mode_register_window()\` call in \`includes/plugins-window/window.php\`.`
3265 );
3266 }
3267 return cfg;
3268 }
3269 function shellFetch(input, init) {
3270 return trackedFetch(input, init, {
3271 windowId: WINDOW_ID,
3272 source: "desktop-mode/plugins-window"
3273 });
3274 }
3275 async function restRequest(url, init = {}) {
3276 const cfg = getConfig();
3277 const { expectJson = true, ...rest } = init;
3278 const response = await shellFetch(url, {
3279 ...rest,
3280 credentials: "same-origin",
3281 headers: {
3282 "X-WP-Nonce": cfg.restNonce,
3283 Accept: "application/json",
3284 ...rest.body ? { "Content-Type": "application/json" } : {},
3285 ...rest.headers ?? {}
3286 }
3287 });
3288 if (!response.ok) {
3289 throw await unpackErrorResponse(response);
3290 }
3291 if (!expectJson) {
3292 return void 0;
3293 }
3294 return await response.json();
3295 }
3296 async function ajaxRequest(action, args = {}, options = {}) {
3297 const cfg = getConfig();
3298 const body = new URLSearchParams();
3299 body.set("action", action);
3300 const nonceField = options.nonceField ?? "_ajax_nonce";
3301 const nonceValue = options.nonceValue ?? cfg.ajaxNonce;
3302 body.set(nonceField, nonceValue);
3303 for (const [key, value] of Object.entries(args)) {
3304 if (value === void 0) {
3305 continue;
3306 }
3307 body.set(key, String(value));
3308 }
3309 const response = await shellFetch(cfg.ajaxUrl, {
3310 method: "POST",
3311 credentials: "same-origin",
3312 headers: {
3313 "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
3314 Accept: "application/json"
3315 },
3316 body
3317 });
3318 const json = await readJsonOrThrow(response);
3319 return unwrapAjaxEnvelope(json, response.status);
3320 }
3321 async function ajaxUpload(action, formData) {
3322 const cfg = getConfig();
3323 formData.set("action", action);
3324 if (!formData.has("_ajax_nonce")) {
3325 formData.set("_ajax_nonce", cfg.ajaxNonce);
3326 }
3327 const response = await shellFetch(cfg.ajaxUrl, {
3328 method: "POST",
3329 credentials: "same-origin",
3330 body: formData
3331 // Don't set Content-Type — the browser appends the boundary.
3332 });
3333 const json = await readJsonOrThrow(response);
3334 return unwrapAjaxEnvelope(json, response.status);
3335 }
3336 async function readJsonOrThrow(response) {
3337 let json;
3338 try {
3339 json = await response.json();
3340 } catch (err) {
3341 throw new Error(
3342 `Server returned ${response.status} with non-JSON body. (${String(err)})`
3343 );
3344 }
3345 if (!response.ok) {
3346 const errPayload = typeof json === "object" && json !== null && "success" in json && json.success === false ? json.data : json;
3347 throw extractAjaxError(errPayload, response.status);
3348 }
3349 return json;
3350 }
3351 function unwrapAjaxEnvelope(json, status) {
3352 if (typeof json === "object" && json !== null && "success" in json) {
3353 const env = json;
3354 if (env.success) {
3355 return env.data ?? null;
3356 }
3357 throw extractAjaxError(env.data, status);
3358 }
3359 return json;
3360 }
3361 function extractAjaxError(data, status) {
3362 if (typeof data === "object" && data !== null) {
3363 const obj = data;
3364 const msg = obj.message ?? obj.errorMessage ?? obj.code ?? obj.errorCode;
3365 if (typeof msg === "string" && msg !== "") {
3366 const err = new Error(msg);
3367 err.code = obj.code ?? obj.errorCode;
3368 err.status = status;
3369 return err;
3370 }
3371 }
3372 return new Error(`Request failed (${status}).`);
3373 }
3374 async function unpackErrorResponse(response) {
3375 let message = `${response.status} ${response.statusText}`;
3376 try {
3377 const json = await response.json();
3378 if (json && typeof json.message === "string" && json.message !== "") {
3379 message = json.message;
3380 }
3381 const err = new Error(message);
3382 err.code = json?.code;
3383 err.status = response.status;
3384 return err;
3385 } catch {
3386 const err = new Error(message);
3387 err.status = response.status;
3388 return err;
3389 }
3390 }
3391 async function fetchInstalledPlugins(opts = {}) {
3392 const cfg = getConfig();
3393 const params = new URLSearchParams({ context: "view", per_page: "100" });
3394 if (opts.force) {
3395 params.set("desktop_mode_force_refresh", "1");
3396 }
3397 const url = joinRestUrl(cfg.restRoot, `wp/v2/plugins?${params.toString()}`);
3398 return restRequest(url, { method: "GET" });
3399 }
3400 async function activateInstalledPlugin(plugin) {
3401 return mutateInstalledPlugin(plugin, { status: "active" });
3402 }
3403 async function deactivateInstalledPlugin(plugin) {
3404 return mutateInstalledPlugin(plugin, { status: "inactive" });
3405 }
3406 async function mutateInstalledPlugin(plugin, body) {
3407 const cfg = getConfig();
3408 return restRequest(
3409 joinRestUrl(cfg.restRoot, `wp/v2/plugins/${encodePluginPath(plugin.plugin)}`),
3410 {
3411 method: "PUT",
3412 body: JSON.stringify(body)
3413 }
3414 );
3415 }
3416 async function deleteInstalledPlugin(plugin) {
3417 const cfg = getConfig();
3418 await restRequest(
3419 joinRestUrl(
3420 cfg.restRoot,
3421 `wp/v2/plugins/${encodePluginPath(plugin.plugin)}?force=true`
3422 ),
3423 {
3424 method: "DELETE",
3425 expectJson: false
3426 }
3427 );
3428 }
3429 function encodePluginPath(plugin) {
3430 return plugin.split("/").map(encodeURIComponent).join("/");
3431 }
3432 async function updateInstalledPlugin(plugin) {
3433 const pluginFile = plugin.plugin.endsWith(".php") ? plugin.plugin : plugin.plugin + ".php";
3434 return ajaxRequest(
3435 "update-plugin",
3436 {
3437 plugin: pluginFile,
3438 slug: plugin.desktop_mode_update_available?.slug || plugin.textdomain || plugin.plugin.split("/")[0]
3439 },
3440 { nonceField: "_ajax_nonce", nonceValue: getConfig().updatesNonce }
3441 );
3442 }
3443 async function toggleAutoUpdate(plugin, state) {
3444 const pluginFile = plugin.plugin.endsWith(".php") ? plugin.plugin : plugin.plugin + ".php";
3445 await ajaxRequest(
3446 "toggle-auto-updates",
3447 {
3448 type: "plugin",
3449 asset: pluginFile,
3450 state
3451 },
3452 { nonceField: "_ajax_nonce", nonceValue: getConfig().updatesNonce }
3453 );
3454 }
3455 async function browsePlugins(args = {}) {
3456 return ajaxRequest("desktop_mode_plugins_browse", {
3457 browse: args.browse,
3458 search: args.search,
3459 tag: args.tag,
3460 page: args.page,
3461 per_page: args.perPage
3462 });
3463 }
3464 async function fetchPluginInfo(slug) {
3465 return ajaxRequest("desktop_mode_plugins_info", { slug });
3466 }
3467 async function fetchFeaturedPlugins() {
3468 return ajaxRequest("desktop_mode_plugins_featured");
3469 }
3470 async function fetchPluginReviews(slug) {
3471 return ajaxRequest("desktop_mode_plugins_reviews", { slug });
3472 }
3473 async function installPluginBySlug(slug) {
3474 return ajaxRequest(
3475 "install-plugin",
3476 { slug },
3477 { nonceField: "_ajax_nonce", nonceValue: getConfig().updatesNonce }
3478 );
3479 }
3480 async function uploadPluginZip(file, options = {}) {
3481 const data = new FormData();
3482 data.set("pluginzip", file);
3483 if (options.overwrite) {
3484 data.set("overwrite", "1");
3485 }
3486 return ajaxUpload("desktop_mode_plugins_upload", data);
3487 }
3488 async function refreshFrameworkMenu() {
3489 const refresh = window.wp?.desktop?.refreshMenu;
3490 if (typeof refresh !== "function") {
3491 return;
3492 }
3493 try {
3494 await refresh();
3495 } catch {
3496 }
3497 }
3498 function isDesktopModeSelf(pluginFile) {
3499 let self = "";
3500 try {
3501 self = getConfig().selfPluginFile;
3502 } catch {
3503 return false;
3504 }
3505 const trim = (s) => s.endsWith(".php") ? s.slice(0, -4) : s;
3506 return self !== "" && trim(self) === trim(pluginFile);
3507 }
3508 function reloadOutOfDesktopMode() {
3509 const target = window.top ?? window;
3510 let dest;
3511 try {
3512 dest = getConfig().adminUrl;
3513 } catch {
3514 dest = "";
3515 }
3516 window.setTimeout(() => {
3517 if (dest) {
3518 try {
3519 target.location.assign(dest);
3520 return;
3521 } catch {
3522 }
3523 window.location.assign(dest);
3524 return;
3525 }
3526 try {
3527 target.location.reload();
3528 } catch {
3529 window.location.reload();
3530 }
3531 }, 800);
3532 }
3533 const tabsStyles = css`:host{display:flex;gap:4px;margin-bottom:10px;border-bottom:1px solid var( --desktop-mode-border,#dcdcde )}`;
3534 const tabPanelStyles = css`:host{display:block}:host( [ hidden ] ){display:none}:host(:focus-visible ){outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:4px;border-radius:4px}`;
3535 const tabStyles = css`:host{display:inline-block}button{appearance:none;padding:6px 10px;border:none;background:transparent;color:var( --desktop-mode-muted,#50575e );font:inherit;font-size:12px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color 0.15s ease,border-color 0.15s ease}button:hover{color:var( --wp-admin-theme-color,#2271b1 )}button:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:2px}:host( [ aria-selected='true' ] ) button{color:var( --wp-admin-theme-color,#2271b1 );border-bottom-color:var( --wp-admin-theme-color,#2271b1 )}`;
3536 const _WpdTab = class _WpdTab extends Component {
3537 render() {
3538 this.setAttribute("role", "tab");
3539 return html`
3540 <button type="button" @click=${() => this._onPick()}>
3541 <slot></slot>
3542 </button>
3543 `;
3544 }
3545 _onPick() {
3546 this.emit("wpd-tab-pick", {
3547 value: this.value
3548 });
3549 }
3550 };
3551 _WpdTab.props = ["value"];
3552 _WpdTab.styles = [tabStyles];
3553 _WpdTab.help = {
3554 title: "Tab",
3555 summary: "Single tab inside a <wpd-tabs> strip. Carries its identifier via `value`; aria-selected + tabindex are mirrored by the parent.",
3556 status: "stable",
3557 since: "0.7.0",
3558 props: [
3559 {
3560 name: "value",
3561 type: "string",
3562 description: "Identifier the tab contributes to the parent strip selection."
3563 }
3564 ],
3565 slots: [
3566 { name: "(default)", description: "Visible tab label." }
3567 ],
3568 events: [
3569 {
3570 name: "wpd-tab-pick",
3571 description: "Internal event bubbled to the parent <wpd-tabs>. Consumers should listen for wpd-tab-change on the strip instead.",
3572 detail: "{ value: string | null }"
3573 }
3574 ]
3575 };
3576 let WpdTab = _WpdTab;
3577 defineComponent("wpd-tab", WpdTab);
3578 const _WpdTabs = class _WpdTabs extends Component {
3579 connectedCallback() {
3580 super.connectedCallback();
3581 this.addEventListener("wpd-tab-pick", (e) => {
3582 const detail = e.detail;
3583 e.stopPropagation();
3584 this.value = detail.value;
3585 this.emit("wpd-tab-change", { value: detail.value });
3586 });
3587 }
3588 /**
3589 * Declarative item-list setter. Replaces the existing `<wpd-tab>`
3590 * children with a fresh set built from a `{ value, label }`
3591 * array. The `value` prop is preserved if it still matches a new
3592 * entry; otherwise it falls back to the first item.
3593 *
3594 * Lets plugins that populate tabs dynamically (route-driven
3595 * admin screens, filtered lists) replace the declarative
3596 * markup with a one-liner:
3597 *
3598 * ```js
3599 * tabs.items = [
3600 * { value: 'calc', label: 'Calc' },
3601 * { value: 'convert', label: 'Convert' },
3602 * ];
3603 * ```
3604 *
3605 * @since 0.5.0
3606 */
3607 set items(list) {
3608 replaceChildren(this, "wpd-tab", list);
3609 const current = this.value;
3610 const stillValid = current !== null && list.some((i) => i.value === current);
3611 if (!stillValid && list.length > 0) {
3612 this.value = list[0].value;
3613 } else {
3614 this.requestUpdate();
3615 }
3616 }
3617 render() {
3618 this.setAttribute("role", "tablist");
3619 const label = this.label || "";
3620 if (label) {
3621 this.setAttribute("aria-label", label);
3622 }
3623 const current = this.value;
3624 queueMicrotask(() => {
3625 const tabs = this.querySelectorAll("wpd-tab");
3626 for (const tab of Array.from(tabs)) {
3627 const v = tab.getAttribute("value");
3628 tab.setAttribute(
3629 "aria-selected",
3630 v === current ? "true" : "false"
3631 );
3632 tab.setAttribute("tabindex", v === current ? "0" : "-1");
3633 }
3634 syncTabpanels(this, current);
3635 });
3636 return html`<slot></slot>`;
3637 }
3638 };
3639 _WpdTabs.props = ["value", "label"];
3640 _WpdTabs.styles = [tabsStyles];
3641 _WpdTabs.help = {
3642 title: "Tabs",
3643 summary: 'Underline-accent tab strip. Pair with sibling <wpd-tabpanel for="…"> elements and the strip auto-toggles their hidden attribute on selection.',
3644 status: "stable",
3645 since: "0.7.0",
3646 props: [
3647 {
3648 name: "value",
3649 type: "string",
3650 description: "Currently active tab value. Mirrored to child <wpd-tab> aria-selected."
3651 },
3652 {
3653 name: "label",
3654 type: "string",
3655 description: "aria-label for the tablist — describe the tab group for assistive tech."
3656 }
3657 ],
3658 slots: [
3659 {
3660 name: "(default)",
3661 description: '<wpd-tab value="…"> children forming the strip.'
3662 }
3663 ],
3664 events: [
3665 {
3666 name: "wpd-tab-change",
3667 description: "Fires when the active tab changes.",
3668 detail: "{ value: string }"
3669 }
3670 ],
3671 example: html`
3672 <wpd-tabs value="one" label="Demo tabs">
3673 <wpd-tab value="one">One</wpd-tab>
3674 <wpd-tab value="two">Two</wpd-tab>
3675 <wpd-tab value="three">Three</wpd-tab>
3676 </wpd-tabs>
3677 <wpd-tabpanel for="one">First panel.</wpd-tabpanel>
3678 <wpd-tabpanel for="two">Second panel.</wpd-tabpanel>
3679 <wpd-tabpanel for="three">Third panel.</wpd-tabpanel>
3680 `
3681 };
3682 let WpdTabs = _WpdTabs;
3683 defineComponent("wpd-tabs", WpdTabs);
3684 const _WpdTabPanel = class _WpdTabPanel extends Component {
3685 // Shadow DOM — the render target for this component is its
3686 // own shadow root, which holds a single `<slot>` that projects
3687 // whatever the caller placed between the `<wpd-tabpanel>` open
3688 // and close tags. Slotted children remain light-DOM descendants
3689 // of the panel element (the slot rendering mechanism doesn't
3690 // move them), so `panel.querySelector(...)` from plugin render
3691 // callbacks keeps working.
3692 //
3693 // Earlier 0.5.0 builds of this component used light DOM with
3694 // a `<slot>` render, which wiped the panel's server-rendered
3695 // template content on first mount — every `render()` writes
3696 // into `_renderRoot`, and with light DOM that's the panel
3697 // itself. Shadow DOM isolates the render surface.
3698 connectedCallback() {
3699 super.connectedCallback();
3700 this.setAttribute("role", "tabpanel");
3701 if (!this.hasAttribute("tabindex")) {
3702 this.setAttribute("tabindex", "0");
3703 }
3704 const owner = findOwningTabs(this);
3705 if (owner) {
3706 syncTabpanels(owner, owner.getAttribute("value"));
3707 }
3708 }
3709 render() {
3710 return html`<slot></slot>`;
3711 }
3712 };
3713 _WpdTabPanel.props = ["for"];
3714 _WpdTabPanel.styles = [tabPanelStyles];
3715 _WpdTabPanel.help = {
3716 title: "Tab panel",
3717 summary: 'Auto-managed panel paired with a sibling <wpd-tabs>. Declares which tab it belongs to via `for="<tab-value>"`; the parent strip toggles `hidden` whenever the active tab changes. role="tabpanel" and tabindex="0" are set automatically.',
3718 status: "stable",
3719 since: "0.5.0",
3720 props: [
3721 {
3722 name: "for",
3723 type: "string",
3724 description: "Matches the `value` of the owning <wpd-tab>. Panel is shown when its parent tabs strip is on that value."
3725 }
3726 ],
3727 slots: [
3728 { name: "(default)", description: "Panel body content." }
3729 ]
3730 };
3731 let WpdTabPanel = _WpdTabPanel;
3732 defineComponent("wpd-tabpanel", WpdTabPanel);
3733 function replaceChildren(host, tag, items) {
3734 const existing = host.querySelectorAll(`:scope > ${tag}`);
3735 for (const el of Array.from(existing)) {
3736 el.remove();
3737 }
3738 for (const item of items) {
3739 const el = document.createElement(tag);
3740 el.setAttribute("value", item.value);
3741 el.textContent = item.label;
3742 host.appendChild(el);
3743 }
3744 }
3745 function findOwningTabs(panel) {
3746 const parent = panel.parentElement;
3747 if (!parent) {
3748 return null;
3749 }
3750 const sibling = parent.querySelector(":scope > wpd-tabs");
3751 if (sibling) {
3752 return sibling;
3753 }
3754 return panel.closest("wpd-tabs");
3755 }
3756 function syncTabpanels(tabs, value) {
3757 const panels = /* @__PURE__ */ new Set();
3758 const parent = tabs.parentElement;
3759 if (parent) {
3760 for (const p of Array.from(
3761 parent.querySelectorAll(":scope > wpd-tabpanel")
3762 )) {
3763 panels.add(p);
3764 }
3765 }
3766 for (const p of Array.from(
3767 tabs.querySelectorAll(":scope > wpd-tabpanel")
3768 )) {
3769 panels.add(p);
3770 }
3771 for (const panel of panels) {
3772 const pfor = panel.getAttribute("for");
3773 const active = pfor !== null && pfor === value;
3774 if (active) {
3775 panel.removeAttribute("hidden");
3776 } else {
3777 panel.setAttribute("hidden", "");
3778 }
3779 panel.setAttribute("aria-hidden", active ? "false" : "true");
3780 }
3781 }
3782 function toast$3(message, duration = 3500) {
3783 const api2 = window.wp?.desktop;
3784 if (api2 && typeof api2.showToast === "function") {
3785 api2.showToast({ message, duration });
3786 return;
3787 }
3788 console.log("[plugins-window]", message);
3789 }
3790 async function confirm$1(opts) {
3791 const api2 = window.wp?.desktop;
3792 if (api2 && typeof api2.confirm === "function") {
3793 return api2.confirm(opts);
3794 }
3795 return Promise.resolve(true);
3796 }
3797 function openDetailFlyout(flyout, slug, hint, callbacks) {
3798 flyout.replaceChildren();
3799 const card = document.createElement("div");
3800 card.className = "desktop-mode-plugins__flyout";
3801 const hero = buildHeroSkeleton(hint);
3802 const tabs = buildTabs();
3803 const body = document.createElement("div");
3804 body.className = "desktop-mode-plugins__flyout-body";
3805 const footer = document.createElement("footer");
3806 footer.className = "desktop-mode-plugins__flyout-footer";
3807 card.append(hero.root, tabs.root, body, footer);
3808 flyout.appendChild(card);
3809 flyout.setAttribute("open", "");
3810 let info = null;
3811 const reviewsCache2 = { loaded: false };
3812 const refreshFooter = () => {
3813 paintFooter(footer, slug, info, callbacks, () => closeFlyout(flyout));
3814 };
3815 refreshFooter();
3816 tabs.onChange((tab) => {
3817 paintTabBody(body, tab, info, slug, reviewsCache2);
3818 });
3819 paintTabBody(body, "overview", info, slug, reviewsCache2);
3820 void (async () => {
3821 try {
3822 info = await fetchPluginInfo(slug);
3823 paintHero(hero, info);
3824 refreshFooter();
3825 const current = tabs.current();
3826 paintTabBody(body, current, info, slug, reviewsCache2);
3827 } catch (err) {
3828 body.innerHTML = "";
3829 const failure = document.createElement("p");
3830 failure.className = "desktop-mode-plugins__flyout-error";
3831 failure.textContent = err instanceof Error ? err.message : __("Could not load plugin details.", "desktop-mode");
3832 body.appendChild(failure);
3833 }
3834 })();
3835 }
3836 function closeFlyout(flyout) {
3837 flyout.removeAttribute("open");
3838 }
3839 function buildHeroSkeleton(hint) {
3840 const root = document.createElement("header");
3841 root.className = "desktop-mode-plugins__flyout-hero";
3842 const banner = document.createElement("div");
3843 banner.className = "desktop-mode-plugins__flyout-banner";
3844 root.appendChild(banner);
3845 const inner = document.createElement("div");
3846 inner.className = "desktop-mode-plugins__flyout-hero-inner";
3847 const icon = document.createElement("div");
3848 icon.className = "desktop-mode-plugins__flyout-hero-icon";
3849 const text = document.createElement("div");
3850 text.className = "desktop-mode-plugins__flyout-hero-text";
3851 const title = document.createElement("h2");
3852 title.className = "desktop-mode-plugins__flyout-hero-title";
3853 const byline = document.createElement("p");
3854 byline.className = "desktop-mode-plugins__flyout-hero-byline";
3855 const meta = document.createElement("div");
3856 meta.className = "desktop-mode-plugins__flyout-hero-meta";
3857 const stars = document.createElement("div");
3858 stars.className = "desktop-mode-plugins__flyout-hero-stars";
3859 meta.appendChild(stars);
3860 text.append(title, byline, meta);
3861 inner.append(icon, text);
3862 root.appendChild(inner);
3863 const close = document.createElement("button");
3864 close.type = "button";
3865 close.className = "desktop-mode-plugins__flyout-close";
3866 close.setAttribute("data-flyout-close", "");
3867 close.setAttribute(
3868 "aria-label",
3869 __("Close plugin details", "desktop-mode")
3870 );
3871 close.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M18 6 L6 18"></path><path d="M6 6 L18 18"></path></svg>';
3872 root.appendChild(close);
3873 if (hint) {
3874 title.textContent = hint.name;
3875 byline.textContent = sprintf(
3876 /* translators: %s: plugin author */
3877 __("by %s", "desktop-mode"),
3878 stripHtml$2(hint.author ?? "")
3879 );
3880 const iconUrl = pickIcon(hint.icons);
3881 if (iconUrl) {
3882 const img = document.createElement("img");
3883 img.src = iconUrl;
3884 img.alt = "";
3885 icon.appendChild(img);
3886 }
3887 stars.appendChild(
3888 buildStarCluster(hint.rating ?? 0, hint.num_ratings ?? 0)
3889 );
3890 }
3891 return { root, icon, title, byline, stars, meta, banner };
3892 }
3893 function paintHero(parts, info) {
3894 parts.title.textContent = info.name;
3895 parts.byline.textContent = sprintf(
3896 /* translators: %s: plugin author */
3897 __("by %s", "desktop-mode"),
3898 stripHtml$2(info.author ?? "")
3899 );
3900 parts.icon.replaceChildren();
3901 const iconUrl = pickIcon(info.icons);
3902 if (iconUrl) {
3903 const img = document.createElement("img");
3904 img.src = iconUrl;
3905 img.alt = "";
3906 parts.icon.appendChild(img);
3907 }
3908 parts.stars.replaceChildren(
3909 buildStarCluster(info.rating ?? 0, info.num_ratings ?? 0)
3910 );
3911 const bannerUrl = info.banners?.high ?? info.banners?.low;
3912 if (bannerUrl) {
3913 parts.banner.style.backgroundImage = `url("${bannerUrl}")`;
3914 parts.banner.classList.add("has-banner");
3915 }
3916 parts.meta.querySelectorAll(":scope > .desktop-mode-plugins__flyout-meta-row").forEach((n) => n.remove());
3917 const metaRow = document.createElement("div");
3918 metaRow.className = "desktop-mode-plugins__flyout-meta-row";
3919 const installs = document.createElement("span");
3920 installs.textContent = sprintf(
3921 /* translators: %s: comma-grouped active install count */
3922 __("%s+ active", "desktop-mode"),
3923 new Intl.NumberFormat().format(info.active_installs ?? 0)
3924 );
3925 const updated = document.createElement("span");
3926 updated.textContent = sprintf(
3927 /* translators: %s: human-readable date string from wp.org */
3928 __("Updated %s", "desktop-mode"),
3929 humanDate$1(info.last_updated)
3930 );
3931 const tested = document.createElement("span");
3932 tested.textContent = info.tested ? sprintf(
3933 /* translators: %s: maximum tested WordPress version */
3934 __("Tested up to WordPress %s", "desktop-mode"),
3935 info.tested
3936 ) : "";
3937 metaRow.append(installs, updated);
3938 if (tested.textContent) {
3939 metaRow.appendChild(tested);
3940 }
3941 parts.meta.appendChild(metaRow);
3942 }
3943 function buildTabs() {
3944 const root = document.createElement("wpd-tabs");
3945 root.className = "desktop-mode-plugins__flyout-tabs";
3946 root.setAttribute("value", "overview");
3947 const labels = [
3948 { value: "overview", label: __("Overview", "desktop-mode") },
3949 { value: "screenshots", label: __("Screenshots", "desktop-mode") },
3950 { value: "reviews", label: __("Reviews", "desktop-mode") },
3951 { value: "changelog", label: __("Changelog", "desktop-mode") },
3952 { value: "faq", label: __("FAQ", "desktop-mode") }
3953 ];
3954 for (const opt of labels) {
3955 const tab = document.createElement("wpd-tab");
3956 tab.setAttribute("value", opt.value);
3957 tab.textContent = opt.label;
3958 root.appendChild(tab);
3959 }
3960 let current = "overview";
3961 const subscribers = /* @__PURE__ */ new Set();
3962 root.addEventListener("wpd-tab-change", (ev) => {
3963 const detail = ev.detail;
3964 const value = detail?.value ?? "overview";
3965 current = value;
3966 for (const cb of subscribers) {
3967 cb(current);
3968 }
3969 });
3970 return {
3971 root,
3972 current: () => current,
3973 onChange: (cb) => subscribers.add(cb)
3974 };
3975 }
3976 function paintTabBody(body, tab, info, slug, reviewsCache2) {
3977 body.replaceChildren();
3978 if (!info) {
3979 body.appendChild(buildSkeletonLines(4));
3980 return;
3981 }
3982 if (tab === "overview") {
3983 body.appendChild(buildHtmlSection(info.sections?.description ?? info.short_description ?? ""));
3984 return;
3985 }
3986 if (tab === "screenshots") {
3987 body.appendChild(buildScreenshots(info.screenshots));
3988 return;
3989 }
3990 if (tab === "changelog") {
3991 body.appendChild(buildHtmlSection(info.sections?.changelog ?? ""));
3992 return;
3993 }
3994 if (tab === "faq") {
3995 body.appendChild(buildHtmlSection(info.sections?.faq ?? ""));
3996 return;
3997 }
3998 if (tab === "reviews") {
3999 body.appendChild(buildRatingsHistogram(info));
4000 const list = document.createElement("div");
4001 list.className = "desktop-mode-plugins__reviews-list";
4002 const loadingLine = document.createElement("p");
4003 loadingLine.className = "desktop-mode-plugins__reviews-loading";
4004 loadingLine.textContent = __("Loading recent reviews…", "desktop-mode");
4005 list.appendChild(loadingLine);
4006 body.appendChild(list);
4007 if (!reviewsCache2.loaded) {
4008 void (async () => {
4009 try {
4010 const resp = await fetchPluginReviews(slug);
4011 list.replaceChildren();
4012 if (!resp.parsed || resp.items.length === 0) {
4013 const fallback = document.createElement("p");
4014 fallback.className = "desktop-mode-plugins__reviews-fallback";
4015 fallback.innerHTML = sprintf(
4016 /* translators: %s: anchor tag with link to wp.org reviews */
4017 __(
4018 "Recent reviews aren’t available right now. %s",
4019 "desktop-mode"
4020 ),
4021 `<a href="https://wordpress.org/plugins/${encodeURIComponent(slug)}/#reviews" target="_blank" rel="noopener">${__(
4022 "Read reviews on WordPress.org ↗",
4023 "desktop-mode"
4024 )}</a>`
4025 );
4026 list.appendChild(fallback);
4027 } else {
4028 for (const item of resp.items) {
4029 list.appendChild(buildReviewCard$1(item));
4030 }
4031 }
4032 reviewsCache2.loaded = true;
4033 } catch {
4034 list.replaceChildren();
4035 const failure = document.createElement("p");
4036 failure.className = "desktop-mode-plugins__reviews-fallback";
4037 failure.textContent = __(
4038 "Could not load reviews.",
4039 "desktop-mode"
4040 );
4041 list.appendChild(failure);
4042 }
4043 })();
4044 }
4045 }
4046 }
4047 function buildHtmlSection(html2) {
4048 const wrap = document.createElement("div");
4049 wrap.className = "desktop-mode-plugins__html";
4050 if (!html2) {
4051 const empty = document.createElement("p");
4052 empty.className = "desktop-mode-plugins__empty-line";
4053 empty.textContent = __("No content available.", "desktop-mode");
4054 wrap.appendChild(empty);
4055 return wrap;
4056 }
4057 wrap.innerHTML = sanitizeHtml$1(html2);
4058 wrap.querySelectorAll("a").forEach((a) => {
4059 a.setAttribute("target", "_blank");
4060 a.setAttribute("rel", "noopener nofollow");
4061 });
4062 return wrap;
4063 }
4064 function buildScreenshots(shots) {
4065 const wrap = document.createElement("div");
4066 wrap.className = "desktop-mode-plugins__screenshots";
4067 const items = shots ? Object.values(shots) : [];
4068 if (items.length === 0) {
4069 const empty = document.createElement("p");
4070 empty.className = "desktop-mode-plugins__empty-line";
4071 empty.textContent = __(
4072 "This plugin doesn’t ship screenshots.",
4073 "desktop-mode"
4074 );
4075 wrap.appendChild(empty);
4076 return wrap;
4077 }
4078 for (const shot of items) {
4079 const fig = document.createElement("figure");
4080 fig.className = "desktop-mode-plugins__screenshot";
4081 const img = document.createElement("img");
4082 img.src = shot.src;
4083 img.loading = "lazy";
4084 img.alt = shot.caption ?? "";
4085 fig.appendChild(img);
4086 if (shot.caption) {
4087 const cap = document.createElement("figcaption");
4088 cap.innerHTML = sanitizeHtml$1(shot.caption);
4089 cap.querySelectorAll("a").forEach((a) => {
4090 a.setAttribute("target", "_blank");
4091 a.setAttribute("rel", "noopener nofollow");
4092 });
4093 fig.appendChild(cap);
4094 }
4095 wrap.appendChild(fig);
4096 }
4097 return wrap;
4098 }
4099 function buildRatingsHistogram(info) {
4100 const wrap = document.createElement("div");
4101 wrap.className = "desktop-mode-plugins__histogram";
4102 const ratings = info.ratings ?? {};
4103 const total = Object.values(ratings).reduce(
4104 (a, b) => a + (typeof b === "number" ? b : 0),
4105 0
4106 );
4107 if (total === 0) {
4108 const empty = document.createElement("p");
4109 empty.className = "desktop-mode-plugins__empty-line";
4110 empty.textContent = __("No ratings yet.", "desktop-mode");
4111 wrap.appendChild(empty);
4112 return wrap;
4113 }
4114 for (let star = 5; star >= 1; star--) {
4115 const count = ratings[String(star)] ?? 0;
4116 const ratio = count / total;
4117 const row = document.createElement("div");
4118 row.className = "desktop-mode-plugins__histogram-row";
4119 const label = document.createElement("span");
4120 label.className = "desktop-mode-plugins__histogram-label";
4121 label.textContent = sprintf(
4122 /* translators: %d: number of stars (1–5) */
4123 __("%d �
4124 ", "desktop-mode"),
4125 star
4126 );
4127 const track = document.createElement("span");
4128 track.className = "desktop-mode-plugins__histogram-track";
4129 const fill = document.createElement("span");
4130 fill.className = "desktop-mode-plugins__histogram-fill";
4131 fill.style.width = `${Math.round(ratio * 100)}%`;
4132 track.appendChild(fill);
4133 const num = document.createElement("span");
4134 num.className = "desktop-mode-plugins__histogram-count";
4135 num.textContent = new Intl.NumberFormat().format(count);
4136 row.append(label, track, num);
4137 wrap.appendChild(row);
4138 }
4139 return wrap;
4140 }
4141 function buildReviewCard$1(item) {
4142 const card = document.createElement("article");
4143 card.className = "desktop-mode-plugins__review";
4144 const head = document.createElement("header");
4145 head.className = "desktop-mode-plugins__review-head";
4146 const author = document.createElement("span");
4147 author.className = "desktop-mode-plugins__review-author";
4148 author.textContent = item.author || __("Anonymous", "desktop-mode");
4149 const star = buildStarCluster(item.stars / 5 * 100, 0);
4150 head.append(author, star);
4151 if (item.date) {
4152 const date = document.createElement("time");
4153 date.className = "desktop-mode-plugins__review-date";
4154 date.textContent = item.date;
4155 head.appendChild(date);
4156 }
4157 const body = document.createElement("p");
4158 body.className = "desktop-mode-plugins__review-excerpt";
4159 body.textContent = item.excerpt;
4160 card.append(head, body);
4161 if (item.url) {
4162 const link = document.createElement("a");
4163 link.href = item.url;
4164 link.target = "_blank";
4165 link.rel = "noopener nofollow";
4166 link.textContent = __("Read on WordPress.org ↗", "desktop-mode");
4167 link.className = "desktop-mode-plugins__review-link";
4168 card.appendChild(link);
4169 }
4170 return card;
4171 }
4172 function buildSkeletonLines(count) {
4173 const wrap = document.createElement("div");
4174 wrap.className = "desktop-mode-plugins__skeleton";
4175 for (let i = 0; i < count; i++) {
4176 const line = document.createElement("span");
4177 line.className = "desktop-mode-plugins__skeleton-line";
4178 line.style.width = `${60 + i * 10 % 40}%`;
4179 wrap.appendChild(line);
4180 }
4181 return wrap;
4182 }
4183 function paintFooter(footer, slug, info, callbacks, close) {
4184 footer.replaceChildren();
4185 const cfg = getConfig();
4186 const installed = callbacks.getInstalled(slug);
4187 const left = document.createElement("div");
4188 left.className = "desktop-mode-plugins__flyout-footer-left";
4189 const wpOrg = document.createElement("a");
4190 wpOrg.href = `https://wordpress.org/plugins/${encodeURIComponent(slug)}/`;
4191 wpOrg.target = "_blank";
4192 wpOrg.rel = "noopener";
4193 wpOrg.className = "desktop-mode-plugins__flyout-wporg";
4194 wpOrg.textContent = __("View on WordPress.org ↗", "desktop-mode");
4195 left.appendChild(wpOrg);
4196 const right = document.createElement("div");
4197 right.className = "desktop-mode-plugins__flyout-footer-right";
4198 if (installed) {
4199 if (cfg.caps.activate) {
4200 if (installed.status === "active" || installed.status === "active-network") {
4201 const btn = button(__("Deactivate", "desktop-mode"), "secondary");
4202 btn.addEventListener("click", () => {
4203 void doDeactivate();
4204 });
4205 right.appendChild(btn);
4206 } else {
4207 const btn = button(__("Activate", "desktop-mode"), "primary");
4208 btn.addEventListener("click", () => {
4209 void doActivate();
4210 });
4211 right.appendChild(btn);
4212 }
4213 }
4214 if (cfg.caps.delete && installed.status === "inactive") {
4215 const btn = button(__("Delete", "desktop-mode"), "danger");
4216 btn.addEventListener("click", () => {
4217 void doDelete();
4218 });
4219 right.appendChild(btn);
4220 }
4221 } else if (cfg.caps.install) {
4222 const btn = button(__("Install", "desktop-mode"), "primary");
4223 btn.addEventListener("click", () => {
4224 void doInstall(btn);
4225 });
4226 right.appendChild(btn);
4227 }
4228 footer.append(left, right);
4229 async function doInstall(btn) {
4230 const originalText = btn.textContent ?? "";
4231 btn.setAttribute("busy", "");
4232 btn.setAttribute("disabled", "");
4233 btn.textContent = __("Installing…", "desktop-mode");
4234 try {
4235 const result = await installPluginBySlug(slug);
4236 toast$3(
4237 sprintf(
4238 /* translators: %s: plugin name */
4239 __("Installed %s.", "desktop-mode"),
4240 info?.name ?? slug
4241 )
4242 );
4243 await callbacks.onPluginInstalled(result.plugin ?? "", slug);
4244 paintFooter(footer, slug, info, callbacks, close);
4245 void refreshFrameworkMenu();
4246 } catch (err) {
4247 btn.removeAttribute("busy");
4248 btn.removeAttribute("disabled");
4249 btn.textContent = originalText;
4250 toast$3(
4251 sprintf(
4252 /* translators: %s: error message */
4253 __("Install failed: %s", "desktop-mode"),
4254 describe$2(err)
4255 ),
4256 6e3
4257 );
4258 }
4259 }
4260 async function doActivate() {
4261 if (!installed) {
4262 return;
4263 }
4264 try {
4265 const updated = await activateInstalledPlugin(installed);
4266 callbacks.onPluginActivated(updated);
4267 toast$3(
4268 sprintf(
4269 /* translators: %s: plugin name */
4270 __("%s activated.", "desktop-mode"),
4271 updated.name || updated.plugin
4272 )
4273 );
4274 paintFooter(footer, slug, info, callbacks, close);
4275 void refreshFrameworkMenu();
4276 } catch (err) {
4277 toast$3(
4278 sprintf(
4279 /* translators: %s: error message */
4280 __("Activation failed: %s", "desktop-mode"),
4281 describe$2(err)
4282 ),
4283 6e3
4284 );
4285 }
4286 }
4287 async function doDeactivate() {
4288 if (!installed) {
4289 return;
4290 }
4291 try {
4292 const updated = await deactivateInstalledPlugin(installed);
4293 callbacks.onPluginDeactivated(updated);
4294 if (isDesktopModeSelf(updated.plugin)) {
4295 toast$3(
4296 __(
4297 "Desktop Mode deactivated. Reloading…",
4298 "desktop-mode"
4299 ),
4300 2e3
4301 );
4302 reloadOutOfDesktopMode();
4303 return;
4304 }
4305 toast$3(
4306 sprintf(
4307 /* translators: %s: plugin name */
4308 __("%s deactivated.", "desktop-mode"),
4309 updated.name || updated.plugin
4310 )
4311 );
4312 paintFooter(footer, slug, info, callbacks, close);
4313 void refreshFrameworkMenu();
4314 } catch (err) {
4315 toast$3(
4316 sprintf(
4317 /* translators: %s: error message */
4318 __("Deactivation failed: %s", "desktop-mode"),
4319 describe$2(err)
4320 ),
4321 6e3
4322 );
4323 }
4324 }
4325 async function doDelete() {
4326 if (!installed) {
4327 return;
4328 }
4329 const ok = await confirm$1({
4330 title: __("Delete plugin?", "desktop-mode"),
4331 message: sprintf(
4332 /* translators: %s: plugin name */
4333 __(
4334 "Permanently delete %s? Its files will be removed from disk. This cannot be undone.",
4335 "desktop-mode"
4336 ),
4337 installed.name || installed.plugin
4338 ),
4339 confirmLabel: __("Delete", "desktop-mode"),
4340 danger: true
4341 });
4342 if (!ok) {
4343 return;
4344 }
4345 try {
4346 await deleteInstalledPlugin(installed);
4347 callbacks.onPluginDeleted(installed);
4348 if (isDesktopModeSelf(installed.plugin)) {
4349 toast$3(
4350 __(
4351 "Desktop Mode deleted. Reloading…",
4352 "desktop-mode"
4353 ),
4354 2e3
4355 );
4356 reloadOutOfDesktopMode();
4357 return;
4358 }
4359 toast$3(
4360 sprintf(
4361 /* translators: %s: plugin name */
4362 __("%s deleted.", "desktop-mode"),
4363 installed.name || installed.plugin
4364 )
4365 );
4366 close();
4367 void refreshFrameworkMenu();
4368 } catch (err) {
4369 toast$3(
4370 sprintf(
4371 /* translators: %s: error message */
4372 __("Delete failed: %s", "desktop-mode"),
4373 describe$2(err)
4374 ),
4375 6e3
4376 );
4377 }
4378 }
4379 }
4380 function button(label, variant) {
4381 const b = document.createElement("wpd-button");
4382 b.setAttribute("variant", variant);
4383 b.textContent = label;
4384 return b;
4385 }
4386 function isSafeUrl(raw) {
4387 const cleaned = Array.from(raw).filter((ch) => ch.charCodeAt(0) > 32).join("").toLowerCase();
4388 const scheme = cleaned.match(/^([a-z][a-z0-9+.-]*):/);
4389 if (!scheme) {
4390 return true;
4391 }
4392 return ["http", "https", "mailto", "tel"].includes(scheme[1]);
4393 }
4394 function sanitizeHtml$1(html2) {
4395 const allowed = /* @__PURE__ */ new Set([
4396 "A",
4397 "ABBR",
4398 "B",
4399 "BLOCKQUOTE",
4400 "BR",
4401 "CODE",
4402 "DD",
4403 "DEL",
4404 "DIV",
4405 "DL",
4406 "DT",
4407 "EM",
4408 "FIGCAPTION",
4409 "FIGURE",
4410 "H1",
4411 "H2",
4412 "H3",
4413 "H4",
4414 "H5",
4415 "H6",
4416 "HR",
4417 "I",
4418 "IMG",
4419 "KBD",
4420 "LI",
4421 "OL",
4422 "P",
4423 "PRE",
4424 "Q",
4425 "S",
4426 "SMALL",
4427 "SPAN",
4428 "STRONG",
4429 "SUB",
4430 "SUP",
4431 "TABLE",
4432 "TBODY",
4433 "TD",
4434 "TFOOT",
4435 "TH",
4436 "THEAD",
4437 "TR",
4438 "U",
4439 "UL"
4440 ]);
4441 const allowedAttrs = /* @__PURE__ */ new Set([
4442 "href",
4443 "src",
4444 "alt",
4445 "title",
4446 "name",
4447 "rel",
4448 "target",
4449 "colspan",
4450 "rowspan"
4451 ]);
4452 const wrap = document.createElement("div");
4453 wrap.innerHTML = html2;
4454 const walker = document.createTreeWalker(wrap, NodeFilter.SHOW_ELEMENT);
4455 const toRemove = [];
4456 let current = walker.currentNode;
4457 while (current) {
4458 const next = walker.nextNode();
4459 if (current === wrap) {
4460 current = next;
4461 continue;
4462 }
4463 if (!allowed.has(current.tagName)) {
4464 toRemove.push(current);
4465 } else {
4466 for (const attr of Array.from(current.attributes)) {
4467 if (!allowedAttrs.has(attr.name.toLowerCase())) {
4468 current.removeAttribute(attr.name);
4469 }
4470 }
4471 if (current.tagName === "A") {
4472 const href = current.getAttribute("href") ?? "";
4473 if (href && !isSafeUrl(href)) {
4474 current.removeAttribute("href");
4475 }
4476 }
4477 if (current.tagName === "IMG") {
4478 const src = current.getAttribute("src") ?? "";
4479 if (src && !isSafeUrl(src)) {
4480 current.removeAttribute("src");
4481 }
4482 }
4483 }
4484 current = next;
4485 }
4486 for (const el of toRemove) {
4487 const text = document.createTextNode(el.textContent ?? "");
4488 el.replaceWith(text);
4489 }
4490 return wrap.innerHTML;
4491 }
4492 function describe$2(err) {
4493 if (err instanceof Error) {
4494 return err.message;
4495 }
4496 return String(err);
4497 }
4498 function stripHtml$2(html2) {
4499 const tmp = document.createElement("div");
4500 tmp.innerHTML = html2;
4501 return tmp.textContent ?? "";
4502 }
4503 function humanDate$1(raw) {
4504 if (!raw) {
4505 return "";
4506 }
4507 const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raw);
4508 if (m) {
4509 const date = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
4510 try {
4511 return date.toLocaleDateString();
4512 } catch {
4513 return raw;
4514 }
4515 }
4516 return raw;
4517 }
4518 const CANARY_TAG = "wpd-confirm-dialog";
4519 let inflight = null;
4520 function isLoaded() {
4521 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
4522 }
4523 function injectScript(scriptUrl) {
4524 return new Promise((resolve, reject) => {
4525 const existing = document.querySelector(
4526 'script[data-desktop-mode-shell-overlays="1"]'
4527 );
4528 const finish = () => {
4529 if (isLoaded()) {
4530 resolve();
4531 return;
4532 }
4533 reject(
4534 new Error(
4535 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
4536 )
4537 );
4538 };
4539 if (existing) {
4540 if (isLoaded()) {
4541 finish();
4542 } else {
4543 existing.addEventListener("load", finish);
4544 existing.addEventListener(
4545 "error",
4546 () => reject(new Error("failed to load shell-overlays bundle"))
4547 );
4548 }
4549 return;
4550 }
4551 const s = document.createElement("script");
4552 s.src = scriptUrl;
4553 s.async = true;
4554 s.dataset.desktopModeShellOverlays = "1";
4555 s.addEventListener("load", finish);
4556 s.addEventListener(
4557 "error",
4558 () => reject(new Error("failed to load shell-overlays bundle"))
4559 );
4560 document.head.appendChild(s);
4561 });
4562 }
4563 function ensureShellOverlaysLoaded(scriptUrl) {
4564 if (isLoaded()) {
4565 return Promise.resolve();
4566 }
4567 if (!scriptUrl) {
4568 return Promise.resolve();
4569 }
4570 if (!inflight) {
4571 inflight = injectScript(scriptUrl);
4572 }
4573 return inflight;
4574 }
4575 function shellOverlaysBundleUrl() {
4576 const cfg = window.desktopModeConfig;
4577 return cfg?.shellOverlaysBundleUrl ?? "";
4578 }
4579 function openWithShellOverlays(isStillCurrent, fn) {
4580 const url = shellOverlaysBundleUrl();
4581 if (isLoaded() || !url) {
4582 fn();
4583 return;
4584 }
4585 void ensureShellOverlaysLoaded(url).then(() => {
4586 if (!isStillCurrent()) {
4587 return;
4588 }
4589 fn();
4590 }).catch((err) => {
4591 if (typeof console !== "undefined") {
4592 console.warn(
4593 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
4594 err
4595 );
4596 }
4597 });
4598 }
4599 async function wpdConfirm(options) {
4600 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
4601 return new Promise((resolve) => {
4602 const dialog = document.createElement("wpd-confirm-dialog");
4603 dialog.setAttribute("open", "");
4604 if (options.title) {
4605 dialog.setAttribute("title", options.title);
4606 }
4607 dialog.setAttribute("message", options.message);
4608 if (options.confirmLabel) {
4609 dialog.setAttribute("confirm-label", options.confirmLabel);
4610 }
4611 if (options.cancelLabel) {
4612 dialog.setAttribute("cancel-label", options.cancelLabel);
4613 }
4614 {
4615 dialog.setAttribute("danger", "");
4616 }
4617 if (options.hideCancel) {
4618 dialog.setAttribute("hide-cancel", "");
4619 }
4620 if (options.dismissable) {
4621 dialog.setAttribute("dismissable", "");
4622 }
4623 const cleanup = (ok) => {
4624 dialog.remove();
4625 resolve(ok);
4626 };
4627 dialog.addEventListener("wpd-confirm", () => cleanup(true));
4628 dialog.addEventListener("wpd-cancel", () => cleanup(false));
4629 document.body.appendChild(dialog);
4630 const inner = dialog.shadowRoot?.querySelector(".dialog");
4631 (inner ?? dialog).focus?.();
4632 });
4633 }
4634 const DEFAULT_DURATION_MS = 4e3;
4635 const FADE_OUT_MS = 200;
4636 function showToast(options) {
4637 const intent = activity.filter(
4638 "desktop-mode/toast-requested",
4639 { ...options }
4640 );
4641 if (!intent || intent.cancel === true) {
4642 return () => void 0;
4643 }
4644 let dismissRequested = false;
4645 let realDismiss = null;
4646 openWithShellOverlays(
4647 () => !dismissRequested,
4648 () => {
4649 realDismiss = renderToast(intent);
4650 }
4651 );
4652 return () => {
4653 dismissRequested = true;
4654 if (realDismiss) {
4655 realDismiss();
4656 }
4657 };
4658 }
4659 function renderToast(intent) {
4660 const container = ensureContainer();
4661 const toast2 = document.createElement("wpd-toast");
4662 toast2.textContent = intent.message;
4663 if (intent.action) {
4664 toast2.setAttribute("action", intent.action.label);
4665 toast2.addEventListener("wpd-toast-action", () => {
4666 intent.action?.onClick();
4667 dismiss();
4668 });
4669 }
4670 if (intent.dismissible) {
4671 toast2.setAttribute("dismissible", "");
4672 toast2.addEventListener("wpd-toast-dismiss", () => {
4673 intent.onDismiss?.();
4674 dismiss();
4675 });
4676 }
4677 container.appendChild(toast2);
4678 let dismissed = false;
4679 let dismissTimer = null;
4680 const dismiss = () => {
4681 if (dismissed) {
4682 return;
4683 }
4684 dismissed = true;
4685 if (dismissTimer !== null) {
4686 window.clearTimeout(dismissTimer);
4687 dismissTimer = null;
4688 }
4689 toast2.setAttribute("state", "out");
4690 window.setTimeout(() => {
4691 toast2.remove();
4692 }, FADE_OUT_MS);
4693 };
4694 requestAnimationFrame(() => {
4695 toast2.setAttribute("state", "in");
4696 });
4697 if (!intent.persistent) {
4698 dismissTimer = window.setTimeout(
4699 dismiss,
4700 intent.duration ?? DEFAULT_DURATION_MS
4701 );
4702 }
4703 activity.publish("desktop-mode/toast-shown", { ...intent });
4704 return dismiss;
4705 }
4706 function ensureContainer() {
4707 const existing = document.querySelector(
4708 "wpd-toast-container"
4709 );
4710 if (existing) {
4711 return existing;
4712 }
4713 const el = document.createElement("wpd-toast-container");
4714 document.body.appendChild(el);
4715 return el;
4716 }
4717 const PLUGINS_CHANGED_TOPIC$3 = "desktop-mode.plugin.changed";
4718 const PLUGINS_CHANGED_SOURCE = "upload-dialog";
4719 function openUploadDialog(host, prefilled, callbacks = {}) {
4720 return new Promise((resolve) => {
4721 const overlay = document.createElement("div");
4722 overlay.className = "desktop-mode-plugins__upload-overlay";
4723 const card = document.createElement("div");
4724 card.className = "desktop-mode-plugins__upload-card";
4725 card.setAttribute("role", "dialog");
4726 card.setAttribute("aria-modal", "true");
4727 card.setAttribute(
4728 "aria-label",
4729 __("Upload a plugin .zip", "desktop-mode")
4730 );
4731 const heading = document.createElement("h2");
4732 heading.className = "desktop-mode-plugins__upload-heading";
4733 heading.textContent = __("Upload a plugin", "desktop-mode");
4734 const lede = document.createElement("p");
4735 lede.className = "desktop-mode-plugins__upload-lede";
4736 lede.textContent = __(
4737 "Pick a .zip file from your computer, or drop one onto the area below.",
4738 "desktop-mode"
4739 );
4740 const dropZone = document.createElement("div");
4741 dropZone.className = "desktop-mode-plugins__upload-dropzone";
4742 dropZone.tabIndex = 0;
4743 dropZone.setAttribute("role", "button");
4744 dropZone.setAttribute(
4745 "aria-label",
4746 __(
4747 "Drop a .zip plugin file here, or click to choose a file.",
4748 "desktop-mode"
4749 )
4750 );
4751 const dropIcon = document.createElement("span");
4752 dropIcon.className = "dashicons dashicons-upload desktop-mode-plugins__upload-icon";
4753 dropIcon.setAttribute("aria-hidden", "true");
4754 const dropHint = document.createElement("p");
4755 dropHint.className = "desktop-mode-plugins__upload-hint";
4756 dropHint.textContent = __(
4757 "Drop your .zip here or click to browse",
4758 "desktop-mode"
4759 );
4760 const fileLabel = document.createElement("p");
4761 fileLabel.className = "desktop-mode-plugins__upload-filename";
4762 fileLabel.hidden = true;
4763 dropZone.append(dropIcon, dropHint, fileLabel);
4764 const input = document.createElement("input");
4765 input.type = "file";
4766 input.accept = ".zip,application/zip,application/x-zip-compressed";
4767 input.style.display = "none";
4768 dropZone.appendChild(input);
4769 const status = document.createElement("p");
4770 status.className = "desktop-mode-plugins__upload-status";
4771 status.hidden = true;
4772 const actions = document.createElement("div");
4773 actions.className = "desktop-mode-plugins__upload-actions";
4774 const cancelBtn = document.createElement("wpd-button");
4775 cancelBtn.setAttribute("variant", "ghost");
4776 cancelBtn.textContent = __("Cancel", "desktop-mode");
4777 const submitBtn = document.createElement("wpd-button");
4778 submitBtn.setAttribute("variant", "primary");
4779 submitBtn.textContent = __("Install", "desktop-mode");
4780 submitBtn.setAttribute("disabled", "");
4781 actions.append(cancelBtn, submitBtn);
4782 card.append(heading, lede, dropZone, status, actions);
4783 overlay.appendChild(card);
4784 host.appendChild(overlay);
4785 const swallowDrag = (ev) => {
4786 ev.preventDefault();
4787 ev.stopPropagation();
4788 };
4789 overlay.addEventListener("dragenter", swallowDrag);
4790 overlay.addEventListener("dragover", swallowDrag);
4791 overlay.addEventListener("drop", swallowDrag);
4792 let pickedFile = null;
4793 let uploading = false;
4794 const setFile = (file) => {
4795 pickedFile = file;
4796 if (file) {
4797 dropZone.classList.add("has-file");
4798 fileLabel.hidden = false;
4799 fileLabel.textContent = sprintf(
4800 /* translators: 1: file name, 2: file size in KB */
4801 __("%1$s · %2$s KB", "desktop-mode"),
4802 file.name,
4803 Math.round(file.size / 1024).toString()
4804 );
4805 submitBtn.removeAttribute("disabled");
4806 } else {
4807 dropZone.classList.remove("has-file");
4808 fileLabel.hidden = true;
4809 submitBtn.setAttribute("disabled", "");
4810 }
4811 };
4812 dropZone.addEventListener("click", (ev) => {
4813 if (ev.target?.tagName === "INPUT") {
4814 return;
4815 }
4816 input.click();
4817 });
4818 dropZone.addEventListener("keydown", (ev) => {
4819 if (ev.key === "Enter" || ev.key === " ") {
4820 ev.preventDefault();
4821 input.click();
4822 }
4823 });
4824 dropZone.addEventListener("dragover", (ev) => {
4825 ev.preventDefault();
4826 ev.stopPropagation();
4827 dropZone.classList.add("is-hovered");
4828 });
4829 dropZone.addEventListener("dragleave", (ev) => {
4830 ev.stopPropagation();
4831 dropZone.classList.remove("is-hovered");
4832 });
4833 dropZone.addEventListener("drop", (ev) => {
4834 ev.preventDefault();
4835 ev.stopPropagation();
4836 dropZone.classList.remove("is-hovered");
4837 const file = ev.dataTransfer?.files?.[0];
4838 if (file && isZip(file)) {
4839 setFile(file);
4840 } else if (file) {
4841 showStatus(
4842 __("Only .zip files are accepted.", "desktop-mode"),
4843 "error"
4844 );
4845 }
4846 });
4847 input.addEventListener("change", () => {
4848 const file = input.files?.[0];
4849 if (file && isZip(file)) {
4850 setFile(file);
4851 }
4852 });
4853 const close = (result) => {
4854 document.removeEventListener("keydown", onKey);
4855 overlay.remove();
4856 resolve(result);
4857 };
4858 const onKey = (ev) => {
4859 if (ev.key === "Escape" && !uploading) {
4860 close(null);
4861 }
4862 };
4863 document.addEventListener("keydown", onKey);
4864 cancelBtn.addEventListener("click", () => {
4865 if (uploading) {
4866 return;
4867 }
4868 close(null);
4869 });
4870 submitBtn.addEventListener("click", () => {
4871 if (!pickedFile || uploading) {
4872 return;
4873 }
4874 void runUpload();
4875 });
4876 overlay.addEventListener("click", (ev) => {
4877 if (ev.target === overlay && !uploading) {
4878 close(null);
4879 }
4880 });
4881 if (prefilled && isZip(prefilled)) {
4882 setFile(prefilled);
4883 }
4884 async function runUpload(overwrite = false) {
4885 if (!pickedFile) {
4886 return;
4887 }
4888 uploading = true;
4889 submitBtn.setAttribute("busy", "");
4890 submitBtn.setAttribute("disabled", "");
4891 cancelBtn.setAttribute("disabled", "");
4892 showStatus(
4893 overwrite ? __("Replacing existing plugin…", "desktop-mode") : __("Uploading and installing…", "desktop-mode"),
4894 "info"
4895 );
4896 try {
4897 const result = await uploadPluginZip(pickedFile, { overwrite });
4898 if (callbacks.onUploaded) {
4899 callbacks.onUploaded(result);
4900 }
4901 broadcast(PLUGINS_CHANGED_TOPIC$3, {
4902 source: PLUGINS_CHANGED_SOURCE,
4903 plugin: result.plugin_file,
4904 action: "install"
4905 });
4906 void refreshFrameworkMenu();
4907 showSuccessPanel(result);
4908 } catch (err) {
4909 const errStatus = err.status;
4910 const errCode = err.code;
4911 if (!overwrite && (errStatus === 409 || errCode === "folder_exists")) {
4912 uploading = false;
4913 submitBtn.removeAttribute("busy");
4914 submitBtn.removeAttribute("disabled");
4915 cancelBtn.removeAttribute("disabled");
4916 showStatus(
4917 __(
4918 "A plugin with the same folder name is already installed.",
4919 "desktop-mode"
4920 ),
4921 "info"
4922 );
4923 const ok = await wpdConfirm({
4924 title: __("Replace existing plugin?", "desktop-mode"),
4925 message: __(
4926 "A plugin with the same folder name is already installed. Replacing it overwrites the installed files. Any local edits to the plugin will be lost. The plugin will keep its activation state.",
4927 "desktop-mode"
4928 ),
4929 confirmLabel: __("Replace", "desktop-mode"),
4930 cancelLabel: __("Cancel", "desktop-mode")
4931 });
4932 if (ok) {
4933 await runUpload(true);
4934 }
4935 return;
4936 }
4937 uploading = false;
4938 submitBtn.removeAttribute("busy");
4939 submitBtn.removeAttribute("disabled");
4940 cancelBtn.removeAttribute("disabled");
4941 const message = err instanceof Error ? err.message : String(err);
4942 showStatus(
4943 sprintf(
4944 /* translators: %s: error message from the upload handler */
4945 __("Upload failed: %s", "desktop-mode"),
4946 message
4947 ),
4948 "error"
4949 );
4950 }
4951 }
4952 function showSuccessPanel(result) {
4953 uploading = false;
4954 dropZone.remove();
4955 input.remove();
4956 actions.remove();
4957 status.hidden = true;
4958 const successHeading = document.createElement("h3");
4959 successHeading.className = "desktop-mode-plugins__upload-success-heading";
4960 successHeading.textContent = __(
4961 "Plugin installed successfully.",
4962 "desktop-mode"
4963 );
4964 const detail = document.createElement("p");
4965 detail.className = "desktop-mode-plugins__upload-success-detail";
4966 const name = result.plugin_name || result.plugin_file;
4967 detail.textContent = result.plugin_version ? sprintf(
4968 /* translators: 1: plugin name 2: plugin version */
4969 __("%1$s %2$s", "desktop-mode"),
4970 name,
4971 result.plugin_version
4972 ) : name;
4973 const successActions = document.createElement("div");
4974 successActions.className = "desktop-mode-plugins__upload-actions";
4975 const closeBtn = document.createElement("wpd-button");
4976 closeBtn.setAttribute("variant", "ghost");
4977 closeBtn.textContent = __("Close", "desktop-mode");
4978 const activateBtn = document.createElement("wpd-button");
4979 activateBtn.setAttribute("variant", "primary");
4980 activateBtn.textContent = __("Activate Plugin", "desktop-mode");
4981 successActions.append(closeBtn, activateBtn);
4982 card.append(successHeading, detail, successActions);
4983 closeBtn.addEventListener("click", () => {
4984 if (uploading) {
4985 return;
4986 }
4987 close(result);
4988 });
4989 activateBtn.addEventListener("click", () => {
4990 if (uploading) {
4991 return;
4992 }
4993 void runActivate();
4994 });
4995 async function runActivate() {
4996 uploading = true;
4997 activateBtn.setAttribute("busy", "");
4998 activateBtn.setAttribute("disabled", "");
4999 closeBtn.setAttribute("disabled", "");
5000 try {
5001 const pluginFile = result.plugin_file.endsWith(".php") ? result.plugin_file.slice(0, -4) : result.plugin_file;
5002 const updated = await activateInstalledPlugin({
5003 plugin: pluginFile,
5004 status: "inactive"
5005 });
5006 if (callbacks.onActivated) {
5007 callbacks.onActivated(result.plugin_file);
5008 }
5009 void refreshFrameworkMenu();
5010 broadcast(PLUGINS_CHANGED_TOPIC$3, {
5011 source: PLUGINS_CHANGED_SOURCE,
5012 plugin: updated.plugin,
5013 action: "activate"
5014 });
5015 showToast({
5016 message: sprintf(
5017 /* translators: %s: plugin name */
5018 __("%s activated.", "desktop-mode"),
5019 name
5020 )
5021 });
5022 uploading = false;
5023 successHeading.textContent = __(
5024 "Plugin activated.",
5025 "desktop-mode"
5026 );
5027 activateBtn.remove();
5028 closeBtn.removeAttribute("disabled");
5029 closeBtn.setAttribute("variant", "primary");
5030 closeBtn.textContent = __("Done", "desktop-mode");
5031 closeBtn.focus?.();
5032 } catch (err) {
5033 uploading = false;
5034 activateBtn.removeAttribute("busy");
5035 activateBtn.removeAttribute("disabled");
5036 closeBtn.removeAttribute("disabled");
5037 const message = err instanceof Error ? err.message : String(err);
5038 status.hidden = false;
5039 status.dataset.tone = "error";
5040 status.textContent = sprintf(
5041 /* translators: %s: error message from the activate handler */
5042 __("Activate failed: %s", "desktop-mode"),
5043 message
5044 );
5045 card.appendChild(status);
5046 }
5047 }
5048 window.setTimeout(() => activateBtn.focus?.(), 16);
5049 }
5050 function showStatus(message, tone) {
5051 status.hidden = false;
5052 status.dataset.tone = tone;
5053 status.textContent = message;
5054 }
5055 window.setTimeout(() => dropZone.focus(), 16);
5056 });
5057 }
5058 function isZip(file) {
5059 if (file.size <= 0) {
5060 return false;
5061 }
5062 const name = file.name.toLowerCase();
5063 if (name.endsWith(".zip")) {
5064 return true;
5065 }
5066 return file.type === "application/zip" || file.type === "application/x-zip-compressed";
5067 }
5068 const segmentedStyles = css`:host{display:inline-flex;padding:3px;background:var( --wpd-segmented-bg,rgba( 0,0,0,0.05 ) );border-radius:7px;gap:2px}`;
5069 const segmentStyles = css`:host{flex:1 1 auto;min-width:0}button{appearance:none;display:block;width:100%;padding:8px 12px;background:transparent;border:0;font:inherit;font-size:13px;color:var( --desktop-mode-muted,#646970 );cursor:pointer;border-radius:5px;transition:background-color 0.12s ease,color 0.12s ease;white-space:nowrap}:host( [ aria-checked='true' ] ) button{background:var( --desktop-mode-window-bg,#fff );color:var( --desktop-mode-text,#1d2327 );box-shadow:0 1px 3px rgba( 0,0,0,0.12 );font-weight:500}`;
5070 const _WpdSegment = class _WpdSegment extends Component {
5071 render() {
5072 this.setAttribute("role", "radio");
5073 return html`
5074 <button type="button" @click=${() => this._onPick()}>
5075 <slot></slot>
5076 </button>
5077 `;
5078 }
5079 _onPick() {
5080 this.emit("wpd-segment-pick", {
5081 value: this.value
5082 });
5083 }
5084 };
5085 _WpdSegment.props = ["value"];
5086 _WpdSegment.styles = [segmentStyles];
5087 _WpdSegment.help = {
5088 title: "Segment",
5089 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
5090 status: "stable",
5091 since: "0.9.0",
5092 props: [
5093 {
5094 name: "value",
5095 type: "string",
5096 description: "Identifier this segment contributes to the parent group selection."
5097 }
5098 ],
5099 slots: [
5100 { name: "(default)", description: "Visible segment label." }
5101 ],
5102 events: [
5103 {
5104 name: "wpd-segment-pick",
5105 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
5106 detail: "{ value: string }"
5107 }
5108 ]
5109 };
5110 let WpdSegment = _WpdSegment;
5111 defineComponent("wpd-segment", WpdSegment);
5112 const _WpdSegmented = class _WpdSegmented extends Component {
5113 connectedCallback() {
5114 super.connectedCallback();
5115 this.addEventListener("wpd-segment-pick", (e) => {
5116 const detail = e.detail;
5117 e.stopPropagation();
5118 this.value = detail.value;
5119 this.emit("wpd-pick", { value: detail.value });
5120 });
5121 }
5122 /**
5123 * Declarative item-list setter. Replaces the existing
5124 * `<wpd-segment>` children with a fresh set built from a
5125 * `{ value, label }` array; preserves the current selection
5126 * when the value still matches an entry, otherwise falls back
5127 * to the first item.
5128 *
5129 * Collapses the pre-0.11 imperative dance (clear children,
5130 * `createElement`, set `textContent`, `appendChild`, then
5131 * `setAttribute('value', …)` on the group — order matters) to
5132 * a single assignment:
5133 *
5134 * ```js
5135 * segmented.items = [
5136 * { value: 'm', label: 'm' },
5137 * { value: 'km', label: 'km' },
5138 * ];
5139 * ```
5140 *
5141 * @since 0.5.0
5142 */
5143 set items(list) {
5144 const existing = this.querySelectorAll(":scope > wpd-segment");
5145 for (const el of Array.from(existing)) {
5146 el.remove();
5147 }
5148 for (const item of list) {
5149 const seg = document.createElement("wpd-segment");
5150 seg.setAttribute("value", item.value);
5151 seg.textContent = item.label;
5152 this.appendChild(seg);
5153 }
5154 const current = this.value;
5155 const stillValid = current !== null && list.some((i) => i.value === current);
5156 if (!stillValid && list.length > 0) {
5157 this.value = list[0].value;
5158 } else {
5159 this.requestUpdate();
5160 }
5161 }
5162 render() {
5163 const label = this.label || "";
5164 if (label) {
5165 this.setAttribute("aria-label", label);
5166 }
5167 this.setAttribute("role", "radiogroup");
5168 const current = this.value;
5169 queueMicrotask(() => {
5170 const segs = this.querySelectorAll("wpd-segment");
5171 for (const seg of Array.from(segs)) {
5172 const v = seg.getAttribute("value");
5173 seg.setAttribute(
5174 "aria-checked",
5175 v === current ? "true" : "false"
5176 );
5177 }
5178 });
5179 return html`<slot></slot>`;
5180 }
5181 };
5182 _WpdSegmented.props = ["value", "label"];
5183 _WpdSegmented.styles = [segmentedStyles];
5184 _WpdSegmented.help = {
5185 title: "Segmented",
5186 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
5187 status: "stable",
5188 since: "0.9.0",
5189 props: [
5190 {
5191 name: "value",
5192 type: "string",
5193 description: "Currently selected segment value. Mirrored onto child aria-checked."
5194 },
5195 {
5196 name: "label",
5197 type: "string",
5198 description: "aria-label for the radiogroup."
5199 }
5200 ],
5201 slots: [
5202 { name: "(default)", description: '<wpd-segment value="…"> children.' }
5203 ],
5204 events: [
5205 {
5206 name: "wpd-pick",
5207 description: "Fires when the selected segment changes.",
5208 detail: "{ value: string }"
5209 }
5210 ],
5211 cssProps: [
5212 { name: "--desktop-mode-window-bg", description: "Pill background." },
5213 { name: "--desktop-mode-text", description: "Active label colour." },
5214 { name: "--desktop-mode-muted", description: "Inactive label colour." }
5215 ],
5216 example: html`
5217 <wpd-segmented value="md" label="Dock size">
5218 <wpd-segment value="sm">Small</wpd-segment>
5219 <wpd-segment value="md">Medium</wpd-segment>
5220 <wpd-segment value="lg">Large</wpd-segment>
5221 </wpd-segmented>
5222 `
5223 };
5224 let WpdSegmented = _WpdSegmented;
5225 defineComponent("wpd-segmented", WpdSegmented);
5226 const textFieldStyles = css`:host{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var( --desktop-mode-text,#1d2327 );min-width:0}:host( [ hidden ] ){display:none}.wpd-text-field__label{font-size:12px;color:var( --desktop-mode-muted,#646970 )}.wpd-text-field__row{position:relative;display:flex;align-items:center;width:100%}input{appearance:none;-webkit-appearance:none;display:block;width:100%;min-width:0;box-sizing:border-box;padding:7px 10px;background:var( --desktop-mode-window-bg,#fff );border:1px solid var( --desktop-mode-border,#dcdcde );border-radius:6px;font:inherit;font-size:13px;color:var( --desktop-mode-text,#1d2327 );transition:border-color 0.12s ease,box-shadow 0.12s ease}.wpd-text-field__suffix{position:absolute;inset-inline-end:10px;top:50%;transform:translateY( -50% );pointer-events:none;font-size:12px;color:var( --desktop-mode-muted,#646970 )}.wpd-text-field__row--has-reveal input{padding-inline-end:36px}.wpd-text-field__reveal{position:absolute;inset-inline-end:0;top:0;bottom:0;width:34px;display:flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var( --desktop-mode-muted,#646970 );cursor:pointer;border-radius:0 6px 6px 0;transition:color 0.12s ease}.wpd-text-field__reveal:hover{color:var( --wp-admin-theme-color,#2271b1 )}.wpd-text-field__reveal:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-2px;border-radius:0 6px 6px 0}.wpd-text-field__reveal:disabled{opacity:0.45;cursor:not-allowed}.wpd-text-field__input--masked{-webkit-text-security:disc;text-security:disc}@supports not ( ( -webkit-text-security:disc ) or ( text-security:disc ) ){.wpd-text-field__input--masked{font-family:text-security-disc,"password",monospace;letter-spacing:0.2em}}input:hover{border-color:var( --desktop-mode-muted,#8c8f94 )}input:focus-visible{outline:none;border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 1px var( --wp-admin-theme-color,#2271b1 )}input:disabled{opacity:0.55;cursor:not-allowed;background:rgba( 0,0,0,0.03 )}input[ aria-invalid='true' ]{border-color:#d63638}input[ aria-invalid='true' ]:focus-visible{box-shadow:0 0 0 1px #d63638}input[ type='number' ]::-webkit-inner-spin-button,input[ type='number' ]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}input[ type='number' ]{-moz-appearance:textfield}`;
5227 const _WpdTextField = class _WpdTextField extends Component {
5228 constructor() {
5229 super(...arguments);
5230 this._revealed = false;
5231 }
5232 connectedCallback() {
5233 super.connectedCallback();
5234 ensureAutoId(this);
5235 }
5236 render() {
5237 const label = this.label || "";
5238 const value = this.value ?? "";
5239 const placeholder = this.placeholder || "";
5240 const disabled = this.disabled !== null;
5241 const readonly = this.readonly !== null;
5242 const declaredAutocomplete = this.autocomplete;
5243 const declaredType = this.type || "text";
5244 const isPassword = declaredType === "password";
5245 let autocomplete = declaredAutocomplete || "off";
5246 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
5247 autocomplete = "new-password";
5248 }
5249 const maxLength = this.maxlength;
5250 const minLength = this.minlength;
5251 const pattern = this.pattern || "";
5252 const name = this.name || "";
5253 const suffix = this.suffix || "";
5254 const invalid = this.invalid !== null;
5255 const reveal = this.reveal !== null;
5256 const isPasswordIntent = declaredType === "password";
5257 const isMasked = isPasswordIntent && !(reveal && this._revealed);
5258 let effectiveType;
5259 if (isPasswordIntent) {
5260 effectiveType = "text";
5261 } else if (reveal && this._revealed) {
5262 effectiveType = "text";
5263 } else {
5264 effectiveType = declaredType;
5265 }
5266 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
5267 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
5268 const hostId = this.id || "wpd-unnamed";
5269 const inputId = `${hostId}__input`;
5270 return html`
5271 ${label ? html`<label
5272 class="wpd-text-field__label"
5273 for=${inputId}
5274 >${label}</label>` : html``}
5275 <span class=${rowClass}>
5276 <input
5277 id=${inputId}
5278 class=${inputClass}
5279 type=${effectiveType}
5280 .value=${value}
5281 placeholder=${placeholder}
5282 ?disabled=${disabled}
5283 ?readonly=${readonly}
5284 autocomplete=${autocomplete}
5285 maxlength=${maxLength ?? ""}
5286 minlength=${minLength ?? ""}
5287 pattern=${pattern}
5288 name=${name}
5289 aria-invalid=${invalid ? "true" : "false"}
5290 aria-label=${label || ""}
5291 @input=${(e) => this._onInput(e)}
5292 @change=${(e) => this._onChange(e)}
5293 @keydown=${(e) => this._onKeyDown(e)}
5294 />
5295 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
5296 ${reveal ? this._renderRevealButton(disabled) : html``}
5297 </span>
5298 `;
5299 }
5300 _renderRevealButton(disabled) {
5301 const label = this._revealed ? "Hide" : "Show";
5302 return html`
5303 <button
5304 type="button"
5305 class="wpd-text-field__reveal"
5306 aria-label=${label}
5307 aria-pressed=${this._revealed ? "true" : "false"}
5308 ?disabled=${disabled}
5309 tabindex="0"
5310 @click=${() => this._onToggleReveal()}
5311 >
5312 ${this._revealed ? _iconEyeOff() : _iconEye()}
5313 </button>
5314 `;
5315 }
5316 _onToggleReveal() {
5317 this._revealed = !this._revealed;
5318 this.requestUpdate();
5319 }
5320 _onInput(e) {
5321 const input = e.target;
5322 this.value = input.value;
5323 this.emit("wpd-input-change", { value: input.value });
5324 }
5325 _onChange(e) {
5326 const input = e.target;
5327 this.emit("wpd-input-commit", { value: input.value });
5328 }
5329 _onKeyDown(e) {
5330 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
5331 const input = e.target;
5332 this.emit("wpd-submit", { value: input.value });
5333 }
5334 }
5335 };
5336 _WpdTextField.props = [
5337 "label",
5338 "value",
5339 "placeholder",
5340 "disabled",
5341 "readonly",
5342 "autocomplete",
5343 "type",
5344 "maxlength",
5345 "minlength",
5346 "pattern",
5347 "name",
5348 "suffix",
5349 "invalid",
5350 "reveal"
5351 ];
5352 _WpdTextField.styles = [textFieldStyles];
5353 _WpdTextField.help = {
5354 title: "Text field",
5355 summary: "Labelled text input primitive. Two-way reflects `value`, emits wpd-input-change per keystroke, wpd-input-commit on blur/change, and wpd-submit on Enter. Optional password reveal toggle.",
5356 status: "stable",
5357 since: "0.5.0",
5358 props: [
5359 { name: "label", type: "string", description: "Visible label above the input." },
5360 { name: "value", type: "string", description: "Current input value; reflected two-way." },
5361 { name: "placeholder", type: "string", description: "Native placeholder string." },
5362 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
5363 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
5364 {
5365 name: "autocomplete",
5366 type: "string",
5367 default: "off",
5368 description: "Forwarded to the native input autocomplete attribute."
5369 },
5370 {
5371 name: "type",
5372 type: "string",
5373 default: "text",
5374 description: "Native input type (text, password, email, search, tel, url)."
5375 },
5376 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
5377 { name: "minlength", type: "integer (string)", description: "Native minlength." },
5378 { name: "pattern", type: "regex string", description: "Native validation pattern." },
5379 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
5380 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
5381 {
5382 name: "invalid",
5383 type: "boolean attribute",
5384 description: "Marks the field aria-invalid and applies the error style."
5385 },
5386 {
5387 name: "reveal",
5388 type: "boolean attribute",
5389 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
5390 }
5391 ],
5392 events: [
5393 {
5394 name: "wpd-input-change",
5395 description: "Fires on every input keystroke.",
5396 detail: "{ value: string }"
5397 },
5398 {
5399 name: "wpd-input-commit",
5400 description: "Fires on the native change event (blur / Enter).",
5401 detail: "{ value: string }"
5402 },
5403 {
5404 name: "wpd-submit",
5405 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
5406 detail: "{ value: string }"
5407 }
5408 ],
5409 cssProps: [
5410 { name: "--desktop-mode-text", description: "Text colour." },
5411 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
5412 { name: "--desktop-mode-border", description: "Input outline." },
5413 { name: "--desktop-mode-window-bg", description: "Input background." }
5414 ],
5415 example: html`
5416 <wpd-stack gap="8">
5417 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
5418 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
5419 </wpd-stack>
5420 `
5421 };
5422 let WpdTextField = _WpdTextField;
5423 defineComponent("wpd-text-field", WpdTextField);
5424 function _iconEye() {
5425 return html`
5426 <svg
5427 viewBox="0 0 16 16"
5428 width="14"
5429 height="14"
5430 fill="none"
5431 stroke="currentColor"
5432 stroke-width="1.5"
5433 stroke-linecap="round"
5434 stroke-linejoin="round"
5435 aria-hidden="true"
5436 focusable="false"
5437 >
5438 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
5439 <circle cx="8" cy="8" r="2" />
5440 </svg>
5441 `;
5442 }
5443 function _iconEyeOff() {
5444 return html`
5445 <svg
5446 viewBox="0 0 16 16"
5447 width="14"
5448 height="14"
5449 fill="none"
5450 stroke="currentColor"
5451 stroke-width="1.5"
5452 stroke-linecap="round"
5453 stroke-linejoin="round"
5454 aria-hidden="true"
5455 focusable="false"
5456 >
5457 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
5458 <circle cx="8" cy="8" r="2" />
5459 <line x1="2" y1="2" x2="14" y2="14" />
5460 </svg>
5461 `;
5462 }
5463 const PLUGINS_CHANGED_TOPIC$2 = "desktop-mode.plugin.changed";
5464 const SOURCE$2 = "browse-view";
5465 function toast$2(message, duration = 3500) {
5466 const api2 = window.wp?.desktop;
5467 if (api2 && typeof api2.showToast === "function") {
5468 api2.showToast({ message, duration });
5469 return;
5470 }
5471 console.log("[plugins-window]", message);
5472 }
5473 function mountBrowseView(host, flyoutEl, bodyEl) {
5474 host.replaceChildren();
5475 const state = {
5476 filter: "featured",
5477 search: "",
5478 page: 1,
5479 totalPages: 0,
5480 loading: false,
5481 exhausted: false,
5482 plugins: [],
5483 installed: /* @__PURE__ */ new Map(),
5484 cardsBySlug: /* @__PURE__ */ new Map()
5485 };
5486 const toolbar = document.createElement("header");
5487 toolbar.className = "desktop-mode-plugins__toolbar";
5488 const left = document.createElement("div");
5489 left.className = "desktop-mode-plugins__toolbar-left";
5490 const segmented = document.createElement("wpd-segmented");
5491 segmented.setAttribute("value", "featured");
5492 const filters = [
5493 { value: "featured", label: __("Featured", "desktop-mode") },
5494 { value: "popular", label: __("Popular", "desktop-mode") },
5495 { value: "recommended", label: __("Recommended", "desktop-mode") },
5496 { value: "favorites", label: __("Favorites", "desktop-mode") },
5497 { value: "new", label: __("New", "desktop-mode") },
5498 { value: "beta", label: __("Beta", "desktop-mode") }
5499 ];
5500 for (const opt of filters) {
5501 const seg = document.createElement("wpd-segment");
5502 seg.setAttribute("value", opt.value);
5503 seg.textContent = opt.label;
5504 segmented.appendChild(seg);
5505 }
5506 segmented.addEventListener("wpd-pick", (ev) => {
5507 const next = ev.detail?.value ?? "featured";
5508 state.filter = next;
5509 void resetAndLoad();
5510 });
5511 const search = document.createElement("wpd-text-field");
5512 search.setAttribute("placeholder", __("Search WordPress.org…", "desktop-mode"));
5513 let searchDebounce;
5514 search.addEventListener("wpd-input-change", (ev) => {
5515 const value = ev.detail?.value ?? "";
5516 window.clearTimeout(searchDebounce);
5517 searchDebounce = window.setTimeout(() => {
5518 state.search = value;
5519 void resetAndLoad();
5520 }, 250);
5521 });
5522 left.append(segmented, search);
5523 const right = document.createElement("div");
5524 right.className = "desktop-mode-plugins__toolbar-trailing";
5525 const cfg = getConfig();
5526 if (cfg.caps.upload) {
5527 const upload = document.createElement("wpd-button");
5528 upload.setAttribute("variant", "secondary");
5529 upload.innerHTML = '<span class="dashicons dashicons-upload" aria-hidden="true"></span> ' + __("Upload Plugin", "desktop-mode");
5530 upload.addEventListener("click", () => {
5531 void openUploadDialog(bodyEl, null, {
5532 onUploaded: () => void refreshInstalled()
5533 });
5534 });
5535 right.appendChild(upload);
5536 }
5537 const refreshButton = document.createElement("wpd-button");
5538 refreshButton.setAttribute("variant", "ghost");
5539 refreshButton.setAttribute("title", __("Refresh", "desktop-mode"));
5540 refreshButton.innerHTML = '<span class="dashicons dashicons-update" aria-hidden="true"></span>';
5541 refreshButton.addEventListener("click", () => {
5542 void refreshInstalled();
5543 void resetAndLoad();
5544 });
5545 right.appendChild(refreshButton);
5546 toolbar.append(left, right);
5547 const gallery = document.createElement("div");
5548 gallery.className = "desktop-mode-plugins__gallery";
5549 const sentinel = document.createElement("div");
5550 sentinel.className = "desktop-mode-plugins__gallery-sentinel";
5551 sentinel.setAttribute("aria-hidden", "true");
5552 const status = document.createElement("p");
5553 status.className = "desktop-mode-plugins__gallery-status";
5554 status.hidden = true;
5555 host.append(toolbar, gallery, status);
5556 const dropOverlay = document.createElement("div");
5557 dropOverlay.className = "desktop-mode-plugins__window-drop";
5558 dropOverlay.setAttribute("aria-hidden", "true");
5559 const dropMsg = document.createElement("p");
5560 dropMsg.textContent = __(
5561 "Drop the .zip to install.",
5562 "desktop-mode"
5563 );
5564 dropOverlay.appendChild(dropMsg);
5565 bodyEl.appendChild(dropOverlay);
5566 let dragDepth = 0;
5567 const isZipDrag = (ev) => Boolean(
5568 ev.dataTransfer?.types.includes("Files")
5569 );
5570 const onDragEnter = (ev) => {
5571 if (!cfg.caps.upload) {
5572 return;
5573 }
5574 if (!isZipDrag(ev)) {
5575 return;
5576 }
5577 dragDepth++;
5578 bodyEl.classList.add("has-zip-dragover");
5579 };
5580 const onDragLeave = (ev) => {
5581 if (!cfg.caps.upload || !isZipDrag(ev)) {
5582 return;
5583 }
5584 dragDepth = Math.max(0, dragDepth - 1);
5585 if (dragDepth === 0) {
5586 bodyEl.classList.remove("has-zip-dragover");
5587 }
5588 };
5589 const onDragOver = (ev) => {
5590 if (cfg.caps.upload && isZipDrag(ev)) {
5591 ev.preventDefault();
5592 }
5593 };
5594 const onDrop = (ev) => {
5595 if (!cfg.caps.upload) {
5596 return;
5597 }
5598 const file = ev.dataTransfer?.files?.[0];
5599 dragDepth = 0;
5600 bodyEl.classList.remove("has-zip-dragover");
5601 if (!file) {
5602 return;
5603 }
5604 ev.preventDefault();
5605 void openUploadDialog(bodyEl, file, {
5606 onUploaded: () => void refreshInstalled()
5607 });
5608 };
5609 bodyEl.addEventListener("dragenter", onDragEnter);
5610 bodyEl.addEventListener("dragleave", onDragLeave);
5611 bodyEl.addEventListener("dragover", onDragOver);
5612 bodyEl.addEventListener("drop", onDrop);
5613 const teardownDropTargets = installPluginDropTargets();
5614 const cardCallbacks = {
5615 onOpen: (slug, hint) => {
5616 if (!flyoutEl) {
5617 return;
5618 }
5619 openDetailFlyout(flyoutEl, slug, hint, {
5620 getInstalled: (s) => state.installed.get(s),
5621 onPluginInstalled: async (pluginFile, slug2) => {
5622 await refreshInstalled();
5623 const card = state.cardsBySlug.get(slug2);
5624 const plugin = state.plugins.find((p) => p.slug === slug2);
5625 if (card && plugin) {
5626 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5627 }
5628 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5629 source: SOURCE$2,
5630 plugin: pluginFile ?? slug2,
5631 action: "install"
5632 });
5633 if (pluginFile) {
5634 console.log("[plugins-window] installed", pluginFile);
5635 }
5636 },
5637 onPluginActivated: (updated) => {
5638 state.installed.set(indexKeyFor$1(updated), updated);
5639 const card = state.cardsBySlug.get(updated.textdomain ?? "");
5640 const plugin = state.plugins.find(
5641 (p) => p.slug === (updated.textdomain ?? "")
5642 );
5643 if (card && plugin) {
5644 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5645 }
5646 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5647 source: SOURCE$2,
5648 plugin: updated.plugin,
5649 action: "activate"
5650 });
5651 },
5652 onPluginDeactivated: (updated) => {
5653 state.installed.set(indexKeyFor$1(updated), updated);
5654 const card = state.cardsBySlug.get(updated.textdomain ?? "");
5655 const plugin = state.plugins.find(
5656 (p) => p.slug === (updated.textdomain ?? "")
5657 );
5658 if (card && plugin) {
5659 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5660 }
5661 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5662 source: SOURCE$2,
5663 plugin: updated.plugin,
5664 action: "deactivate"
5665 });
5666 },
5667 onPluginDeleted: (deleted) => {
5668 const key = indexKeyFor$1(deleted);
5669 state.installed.delete(key);
5670 const card = state.cardsBySlug.get(deleted.textdomain ?? "");
5671 const plugin = state.plugins.find(
5672 (p) => p.slug === (deleted.textdomain ?? "")
5673 );
5674 if (card && plugin) {
5675 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5676 }
5677 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5678 source: SOURCE$2,
5679 plugin: deleted.plugin,
5680 action: "delete"
5681 });
5682 }
5683 });
5684 },
5685 onInstall: async (plugin, card) => {
5686 const cta = card.querySelector("[data-plugin-card-cta]");
5687 const ctaOriginalText = cta?.textContent ?? "";
5688 cta?.setAttribute("busy", "");
5689 cta?.setAttribute("disabled", "");
5690 if (cta) {
5691 cta.textContent = __("Installing…", "desktop-mode");
5692 }
5693 try {
5694 await installPluginBySlug(plugin.slug);
5695 await refreshInstalled();
5696 toast$2(
5697 sprintf(
5698 /* translators: %s: plugin name */
5699 __("Installed %s.", "desktop-mode"),
5700 plugin.name
5701 )
5702 );
5703 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5704 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5705 source: SOURCE$2,
5706 plugin: plugin.slug,
5707 action: "install"
5708 });
5709 void refreshFrameworkMenu();
5710 } catch (err) {
5711 cta?.removeAttribute("busy");
5712 cta?.removeAttribute("disabled");
5713 if (cta) {
5714 cta.textContent = ctaOriginalText;
5715 }
5716 toast$2(
5717 sprintf(
5718 /* translators: %s: error message */
5719 __("Install failed: %s", "desktop-mode"),
5720 describe$1(err)
5721 ),
5722 6e3
5723 );
5724 }
5725 },
5726 onActivate: async (installed, card) => {
5727 const cta = card.querySelector("[data-plugin-card-cta]");
5728 const ctaOriginalText = cta?.textContent ?? "";
5729 cta?.setAttribute("busy", "");
5730 cta?.setAttribute("disabled", "");
5731 if (cta) {
5732 cta.textContent = __("Activating…", "desktop-mode");
5733 }
5734 try {
5735 const updated = await activateInstalledPlugin(installed);
5736 state.installed.set(indexKeyFor$1(updated), updated);
5737 toast$2(
5738 sprintf(
5739 /* translators: %s: plugin name */
5740 __("%s activated.", "desktop-mode"),
5741 updated.name || updated.plugin
5742 )
5743 );
5744 const plugin = state.plugins.find(
5745 (p) => p.slug === (updated.textdomain ?? "")
5746 );
5747 if (plugin) {
5748 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5749 }
5750 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5751 source: SOURCE$2,
5752 plugin: updated.plugin,
5753 action: "activate"
5754 });
5755 void refreshFrameworkMenu();
5756 } catch (err) {
5757 cta?.removeAttribute("busy");
5758 cta?.removeAttribute("disabled");
5759 if (cta) {
5760 cta.textContent = ctaOriginalText;
5761 }
5762 toast$2(
5763 sprintf(
5764 /* translators: %s: error message */
5765 __("Activation failed: %s", "desktop-mode"),
5766 describe$1(err)
5767 ),
5768 6e3
5769 );
5770 }
5771 }
5772 };
5773 const observer = new IntersectionObserver(
5774 (entries) => {
5775 for (const entry of entries) {
5776 if (entry.isIntersecting) {
5777 void loadMore();
5778 }
5779 }
5780 },
5781 { root: gallery, rootMargin: "240px", threshold: 0 }
5782 );
5783 observer.observe(sentinel);
5784 void refreshInstalled();
5785 void resetAndLoad();
5786 async function refreshInstalled() {
5787 try {
5788 const rows = await fetchInstalledPlugins();
5789 state.installed = new Map(
5790 rows.map((r) => [indexKeyFor$1(r), r])
5791 );
5792 for (const [slug, card] of state.cardsBySlug) {
5793 const plugin = state.plugins.find((p) => p.slug === slug);
5794 if (plugin) {
5795 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5796 }
5797 }
5798 } catch {
5799 }
5800 }
5801 async function resetAndLoad() {
5802 state.page = 1;
5803 state.totalPages = 0;
5804 state.exhausted = false;
5805 state.plugins = [];
5806 state.cardsBySlug.clear();
5807 gallery.replaceChildren();
5808 for (let i = 0; i < 6; i++) {
5809 gallery.appendChild(buildSkeletonCard$1());
5810 }
5811 gallery.appendChild(sentinel);
5812 await loadMore();
5813 }
5814 const inflightSkeletons = [];
5815 function showInflightLoader() {
5816 if (inflightSkeletons.length > 0) {
5817 return;
5818 }
5819 for (let i = 0; i < 4; i++) {
5820 const skel = buildSkeletonCard$1();
5821 gallery.insertBefore(skel, sentinel);
5822 inflightSkeletons.push(skel);
5823 }
5824 }
5825 function clearInflightLoader() {
5826 for (const skel of inflightSkeletons) {
5827 skel.remove();
5828 }
5829 inflightSkeletons.length = 0;
5830 }
5831 async function loadMore() {
5832 if (state.loading || state.exhausted) {
5833 return;
5834 }
5835 state.loading = true;
5836 if (state.page > 1) {
5837 showInflightLoader();
5838 }
5839 try {
5840 const data = await browsePlugins({
5841 browse: state.search === "" ? state.filter : void 0,
5842 search: state.search === "" ? void 0 : state.search,
5843 page: state.page,
5844 perPage: 24
5845 });
5846 if (state.page === 1) {
5847 gallery.replaceChildren();
5848 gallery.appendChild(sentinel);
5849 }
5850 const info = data.info ?? {};
5851 if (typeof info.pages === "number" && info.pages > 0) {
5852 state.totalPages = info.pages;
5853 }
5854 const incoming = data.plugins ?? [];
5855 if (incoming.length === 0) {
5856 state.exhausted = true;
5857 if (state.page === 1) {
5858 showStatus(__("No plugins matched.", "desktop-mode"));
5859 }
5860 return;
5861 }
5862 for (const plugin of incoming) {
5863 if (!plugin?.slug) {
5864 continue;
5865 }
5866 if (state.cardsBySlug.has(plugin.slug)) {
5867 continue;
5868 }
5869 const card = buildCard(plugin, state.installed, cardCallbacks);
5870 makeCardDraggable(card, plugin);
5871 gallery.insertBefore(card, sentinel);
5872 state.cardsBySlug.set(plugin.slug, card);
5873 state.plugins.push(plugin);
5874 }
5875 state.page++;
5876 if (state.totalPages > 0 && state.page > state.totalPages) {
5877 state.exhausted = true;
5878 } else if (state.totalPages === 0 && incoming.length < 24) {
5879 state.exhausted = true;
5880 }
5881 hideStatus();
5882 } catch (err) {
5883 showStatus(
5884 sprintf(
5885 /* translators: %s: error message */
5886 __("Could not load plugins: %s", "desktop-mode"),
5887 describe$1(err)
5888 )
5889 );
5890 } finally {
5891 clearInflightLoader();
5892 state.loading = false;
5893 }
5894 }
5895 function showStatus(message) {
5896 status.hidden = false;
5897 status.textContent = message;
5898 }
5899 function hideStatus() {
5900 status.hidden = true;
5901 status.textContent = "";
5902 }
5903 const unsubscribePluginsChanged = subscribe(
5904 PLUGINS_CHANGED_TOPIC$2,
5905 (payload) => {
5906 if (payload?.source === SOURCE$2) {
5907 return;
5908 }
5909 void refreshInstalled();
5910 }
5911 );
5912 return () => {
5913 unsubscribePluginsChanged();
5914 observer.disconnect();
5915 bodyEl.removeEventListener("dragenter", onDragEnter);
5916 bodyEl.removeEventListener("dragleave", onDragLeave);
5917 bodyEl.removeEventListener("dragover", onDragOver);
5918 bodyEl.removeEventListener("drop", onDrop);
5919 dropOverlay.remove();
5920 teardownDropTargets();
5921 host.replaceChildren();
5922 };
5923 }
5924 function buildSkeletonCard$1() {
5925 const card = document.createElement("wpd-card");
5926 card.classList.add(
5927 "desktop-mode-plugins__card",
5928 "desktop-mode-plugins__card--skeleton"
5929 );
5930 card.setAttribute("aria-hidden", "true");
5931 for (let i = 0; i < 4; i++) {
5932 const line = document.createElement("span");
5933 line.className = "desktop-mode-plugins__skeleton-line";
5934 line.style.width = `${50 + i * 17 % 50}%`;
5935 card.appendChild(line);
5936 }
5937 return card;
5938 }
5939 function indexKeyFor$1(plugin) {
5940 return plugin.textdomain || plugin.plugin;
5941 }
5942 function describe$1(err) {
5943 if (err instanceof Error) {
5944 return err.message;
5945 }
5946 return String(err);
5947 }
5948 const styles$8 = 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 )}`;
5949 const _WpdRibbon = class _WpdRibbon extends Component {
5950 render() {
5951 return html`<span class="banner" part="banner"><slot></slot></span>`;
5952 }
5953 };
5954 _WpdRibbon.props = ["placement", "tone"];
5955 _WpdRibbon.styles = [styles$8];
5956 _WpdRibbon.help = {
5957 title: "Ribbon",
5958 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.",
5959 status: "experimental",
5960 since: "0.8.6",
5961 props: [
5962 {
5963 name: "placement",
5964 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
5965 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
5966 },
5967 {
5968 name: "tone",
5969 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
5970 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
5971 }
5972 ],
5973 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
5974 cssProps: [
5975 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
5976 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
5977 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
5978 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
5979 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
5980 { name: "--wpd-ribbon-fg", default: "#fff" },
5981 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
5982 { name: "--wpd-ribbon-padding", default: "4px 0" },
5983 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
5984 { name: "--wpd-ribbon-tracking", default: "0.06em" },
5985 { name: "--wpd-ribbon-z", default: "2" }
5986 ],
5987 example: html`
5988 <div
5989 style="position: relative; width: 240px; height: 120px;
5990 border: 1px solid #ccc; border-radius: 8px;
5991 padding: 16px; box-sizing: border-box;"
5992 >
5993 <wpd-ribbon>Featured</wpd-ribbon>
5994 Card body…
5995 </div>
5996 `
5997 };
5998 let WpdRibbon = _WpdRibbon;
5999 defineComponent("wpd-ribbon", WpdRibbon);
6000 const PLUGINS_CHANGED_TOPIC$1 = "desktop-mode.plugin.changed";
6001 const SOURCE$1 = "featured-view";
6002 function toast$1(message, duration = 3500) {
6003 const api2 = window.wp?.desktop;
6004 if (api2 && typeof api2.showToast === "function") {
6005 api2.showToast({ message, duration });
6006 return;
6007 }
6008 console.log("[plugins-window]", message);
6009 }
6010 function mountFeaturedView(host, flyoutEl) {
6011 host.replaceChildren();
6012 const state = {
6013 plugins: [],
6014 installed: /* @__PURE__ */ new Map(),
6015 cardsBySlug: /* @__PURE__ */ new Map(),
6016 loading: true
6017 };
6018 const intro = document.createElement("header");
6019 intro.className = "desktop-mode-plugins__featured-intro";
6020 const heading = document.createElement("h2");
6021 heading.className = "desktop-mode-plugins__featured-heading";
6022 heading.textContent = __("Made for Desktop Mode", "desktop-mode");
6023 const description = document.createElement("p");
6024 description.className = "desktop-mode-plugins__featured-blurb";
6025 description.textContent = __(
6026 "Plugins that extend Desktop Mode — desktop decorations, native windows, widgets, and other companions.",
6027 "desktop-mode"
6028 );
6029 intro.append(heading, description);
6030 const gallery = document.createElement("div");
6031 gallery.className = "desktop-mode-plugins__gallery";
6032 const status = document.createElement("p");
6033 status.className = "desktop-mode-plugins__gallery-status";
6034 status.hidden = true;
6035 host.append(intro, gallery, status);
6036 const cardCallbacks = {
6037 onOpen: (slug, hint) => {
6038 if (!flyoutEl) {
6039 return;
6040 }
6041 openDetailFlyout(flyoutEl, slug, hint, {
6042 getInstalled: (s) => state.installed.get(s),
6043 onPluginInstalled: async (pluginFile, slug2) => {
6044 await refreshInstalled();
6045 repaintSlugCard(slug2);
6046 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6047 source: SOURCE$1,
6048 plugin: pluginFile ?? slug2,
6049 action: "install"
6050 });
6051 },
6052 onPluginActivated: (updated) => {
6053 state.installed.set(indexKeyFor(updated), updated);
6054 repaintSlugCard(updated.textdomain ?? "");
6055 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6056 source: SOURCE$1,
6057 plugin: updated.plugin,
6058 action: "activate"
6059 });
6060 },
6061 onPluginDeactivated: (updated) => {
6062 state.installed.set(indexKeyFor(updated), updated);
6063 repaintSlugCard(updated.textdomain ?? "");
6064 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6065 source: SOURCE$1,
6066 plugin: updated.plugin,
6067 action: "deactivate"
6068 });
6069 },
6070 onPluginDeleted: (deleted) => {
6071 state.installed.delete(indexKeyFor(deleted));
6072 repaintSlugCard(deleted.textdomain ?? "");
6073 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6074 source: SOURCE$1,
6075 plugin: deleted.plugin,
6076 action: "delete"
6077 });
6078 }
6079 });
6080 },
6081 onInstall: async (plugin, card) => {
6082 const cta = card.querySelector("[data-plugin-card-cta]");
6083 const originalText = cta?.textContent ?? "";
6084 cta?.setAttribute("busy", "");
6085 cta?.setAttribute("disabled", "");
6086 if (cta) {
6087 cta.textContent = __("Installing…", "desktop-mode");
6088 }
6089 try {
6090 await installPluginBySlug(plugin.slug);
6091 await refreshInstalled();
6092 toast$1(
6093 sprintf(
6094 /* translators: %s: plugin name */
6095 __("Installed %s.", "desktop-mode"),
6096 plugin.name
6097 )
6098 );
6099 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6100 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6101 source: SOURCE$1,
6102 plugin: plugin.slug,
6103 action: "install"
6104 });
6105 void refreshFrameworkMenu();
6106 } catch (err) {
6107 cta?.removeAttribute("busy");
6108 cta?.removeAttribute("disabled");
6109 if (cta) {
6110 cta.textContent = originalText;
6111 }
6112 toast$1(
6113 sprintf(
6114 /* translators: %s: error message */
6115 __("Install failed: %s", "desktop-mode"),
6116 formatError(err)
6117 ),
6118 6e3
6119 );
6120 }
6121 },
6122 onActivate: async (installed, card) => {
6123 const cta = card.querySelector("[data-plugin-card-cta]");
6124 const originalText = cta?.textContent ?? "";
6125 cta?.setAttribute("busy", "");
6126 cta?.setAttribute("disabled", "");
6127 if (cta) {
6128 cta.textContent = __("Activating…", "desktop-mode");
6129 }
6130 try {
6131 const updated = await activateInstalledPlugin(installed);
6132 state.installed.set(indexKeyFor(updated), updated);
6133 toast$1(
6134 sprintf(
6135 /* translators: %s: plugin name */
6136 __("%s activated.", "desktop-mode"),
6137 updated.name || updated.plugin
6138 )
6139 );
6140 const plugin = state.plugins.find(
6141 (p) => p.slug === (updated.textdomain ?? "")
6142 );
6143 if (plugin) {
6144 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6145 }
6146 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6147 source: SOURCE$1,
6148 plugin: updated.plugin,
6149 action: "activate"
6150 });
6151 void refreshFrameworkMenu();
6152 } catch (err) {
6153 cta?.removeAttribute("busy");
6154 cta?.removeAttribute("disabled");
6155 if (cta) {
6156 cta.textContent = originalText;
6157 }
6158 toast$1(
6159 sprintf(
6160 /* translators: %s: error message */
6161 __("Activation failed: %s", "desktop-mode"),
6162 formatError(err)
6163 ),
6164 6e3
6165 );
6166 }
6167 }
6168 };
6169 void load();
6170 async function load() {
6171 state.loading = true;
6172 paintSkeletons();
6173 try {
6174 const [featured, installed] = await Promise.all([
6175 fetchFeaturedPlugins(),
6176 fetchInstalledPlugins().catch(() => [])
6177 ]);
6178 state.installed = new Map(
6179 installed.map((r) => [indexKeyFor(r), r])
6180 );
6181 state.plugins = featured.plugins ?? [];
6182 renderGallery();
6183 if (state.plugins.length === 0) {
6184 showStatus(__("No featured plugins yet.", "desktop-mode"));
6185 } else {
6186 hideStatus();
6187 }
6188 } catch (err) {
6189 gallery.replaceChildren();
6190 showStatus(
6191 sprintf(
6192 /* translators: %s: error message */
6193 __("Could not load featured plugins: %s", "desktop-mode"),
6194 formatError(err)
6195 )
6196 );
6197 } finally {
6198 state.loading = false;
6199 }
6200 }
6201 function paintSkeletons() {
6202 gallery.replaceChildren();
6203 state.cardsBySlug.clear();
6204 for (let i = 0; i < 3; i++) {
6205 gallery.appendChild(buildSkeletonCard());
6206 }
6207 }
6208 function renderGallery() {
6209 gallery.replaceChildren();
6210 state.cardsBySlug.clear();
6211 for (const plugin of state.plugins) {
6212 if (!plugin?.slug) {
6213 continue;
6214 }
6215 const card = buildCard(plugin, state.installed, cardCallbacks);
6216 if (plugin.featured) {
6217 card.classList.add("desktop-mode-plugins__card--featured");
6218 const ribbon = document.createElement("wpd-ribbon");
6219 ribbon.textContent = __("Featured", "desktop-mode");
6220 card.prepend(ribbon);
6221 }
6222 makeCardDraggable(card, plugin);
6223 gallery.appendChild(card);
6224 state.cardsBySlug.set(plugin.slug, card);
6225 }
6226 }
6227 function repaintSlugCard(slug) {
6228 if (!slug) {
6229 return;
6230 }
6231 const card = state.cardsBySlug.get(slug);
6232 const plugin = state.plugins.find((p) => p.slug === slug);
6233 if (card && plugin) {
6234 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6235 }
6236 }
6237 async function refreshInstalled() {
6238 try {
6239 const rows = await fetchInstalledPlugins();
6240 state.installed = new Map(
6241 rows.map((r) => [indexKeyFor(r), r])
6242 );
6243 for (const [slug, card] of state.cardsBySlug) {
6244 const plugin = state.plugins.find((p) => p.slug === slug);
6245 if (plugin) {
6246 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6247 }
6248 }
6249 } catch {
6250 }
6251 }
6252 function showStatus(message) {
6253 status.hidden = false;
6254 status.textContent = message;
6255 }
6256 function hideStatus() {
6257 status.hidden = true;
6258 status.textContent = "";
6259 }
6260 const unsubscribePluginsChanged = subscribe(
6261 PLUGINS_CHANGED_TOPIC$1,
6262 (payload) => {
6263 if (payload?.source === SOURCE$1) {
6264 return;
6265 }
6266 void refreshInstalled();
6267 }
6268 );
6269 return () => {
6270 unsubscribePluginsChanged();
6271 host.replaceChildren();
6272 };
6273 }
6274 function buildSkeletonCard() {
6275 const card = document.createElement("wpd-card");
6276 card.classList.add(
6277 "desktop-mode-plugins__card",
6278 "desktop-mode-plugins__card--skeleton"
6279 );
6280 card.setAttribute("aria-hidden", "true");
6281 for (let i = 0; i < 4; i++) {
6282 const line = document.createElement("span");
6283 line.className = "desktop-mode-plugins__skeleton-line";
6284 line.style.width = `${50 + i * 17 % 50}%`;
6285 card.appendChild(line);
6286 }
6287 return card;
6288 }
6289 function indexKeyFor(plugin) {
6290 return plugin.textdomain || plugin.plugin;
6291 }
6292 function formatError(err) {
6293 if (err instanceof Error) {
6294 return err.message;
6295 }
6296 return String(err);
6297 }
6298 const queue = [];
6299 let inFlight = false;
6300 function enqueueUpdateJob(run) {
6301 return new Promise((resolve, reject) => {
6302 queue.push({
6303 run,
6304 resolve,
6305 reject
6306 });
6307 void drain();
6308 });
6309 }
6310 async function drain() {
6311 if (inFlight) {
6312 return;
6313 }
6314 const job = queue.shift();
6315 if (!job) {
6316 return;
6317 }
6318 inFlight = true;
6319 try {
6320 const value = await job.run();
6321 job.resolve(value);
6322 } catch (err) {
6323 job.reject(err);
6324 } finally {
6325 inFlight = false;
6326 void Promise.resolve().then(drain);
6327 }
6328 }
6329 const WP_ORG_ASSET_RE = /^(https:\/\/ps\.w\.org\/[a-z0-9-]+\/assets\/)icon\.svg$/i;
6330 function buildCandidates(initialUrl) {
6331 const match = initialUrl.match(WP_ORG_ASSET_RE);
6332 if (!match) {
6333 return [initialUrl];
6334 }
6335 const base = match[1];
6336 return [
6337 initialUrl,
6338 base + "icon-256x256.png",
6339 base + "icon-256x256.gif",
6340 base + "icon-128x128.png",
6341 base + "icon-128x128.gif"
6342 ];
6343 }
6344 function attachIconFallback(img, initialUrl, onExhausted) {
6345 const candidates = buildCandidates(initialUrl);
6346 let index = 0;
6347 img.addEventListener("error", () => {
6348 index += 1;
6349 if (index < candidates.length) {
6350 img.src = candidates[index];
6351 return;
6352 }
6353 onExhausted();
6354 });
6355 return candidates[0];
6356 }
6357 const styles$7 = css`:host{display:inline-flex;max-width:100%;vertical-align:middle}:host( [ hidden ] ){display:none}.wpd-chip{display:inline-flex;align-items:center;gap:var( --wpd-chip-gap,4px );padding:var( --wpd-chip-padding,2px 8px );border-radius:var( --wpd-chip-radius,999px );font-size:var( --wpd-chip-font-size,12px );line-height:var( --wpd-chip-line-height,1.6 );font-weight:var( --wpd-chip-font-weight,500 );background:var( --wpd-chip-bg,#f0f0f1 );color:var( --wpd-chip-fg,#1d2327 );border:var( --wpd-chip-border,1px solid transparent );max-width:100%;box-sizing:border-box;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease,transform 0.12s ease,opacity 0.12s ease}:host( [ tone='accent' ] ) .wpd-chip{background:var( --wpd-chip-bg,color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 14%,transparent ) );color:var( --wpd-chip-fg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ tone='positive' ] ) .wpd-chip{background:var( --wpd-chip-bg,rgba( 30,132,73,0.14 ) );color:var( --wpd-chip-fg,#1d6f42 )}:host( [ tone='warning' ] ) .wpd-chip{background:var( --wpd-chip-bg,rgba( 217,119,6,0.18 ) );color:var( --wpd-chip-fg,#8a4a06 )}:host( [ tone='danger' ] ) .wpd-chip{background:var( --wpd-chip-bg,rgba( 214,54,56,0.14 ) );color:var( --wpd-chip-fg,#a02622 )}:host( [ pending ] ) .wpd-chip{opacity:0.65;animation:wpd-chip-pulse 1.2s ease-in-out infinite}@keyframes wpd-chip-pulse{0%,100%{opacity:0.55}50%{opacity:0.95}}.wpd-chip__label{max-width:var( --wpd-chip-label-max,220px );overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wpd-chip__icon{display:inline-flex;align-items:center;flex-shrink:0}.wpd-chip__icon::slotted( * ){display:inline-flex}.wpd-chip__dismiss{appearance:none;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:16px;height:16px;margin-inline-start:2px;padding:0;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer;opacity:0.55;transition:opacity 0.12s ease,background-color 0.12s ease}.wpd-chip__dismiss:hover,.wpd-chip__dismiss:focus-visible{opacity:1;background:rgba( 0,0,0,0.12 );outline:none}.wpd-chip__dismiss:focus-visible{box-shadow:0 0 0 2px var( --wp-admin-theme-color,#2271b1 )}.wpd-chip__dismiss[ disabled ]{opacity:0.35;cursor:not-allowed}.wpd-chip__dismiss svg{display:block;width:10px;height:10px}:host( [ disabled ] ) .wpd-chip{opacity:0.55;cursor:not-allowed}:host( [ size='compact' ] ) .wpd-chip{padding:var( --wpd-chip-padding,1px 6px );font-size:var( --wpd-chip-font-size,11px )}`;
6358 const _WpdChip = class _WpdChip extends Component {
6359 constructor() {
6360 super(...arguments);
6361 this._onHostKeyDown = (e) => {
6362 const dismissible = this.dismissible !== null;
6363 if (!dismissible) {
6364 return;
6365 }
6366 if (e.key === "Backspace" || e.key === "Delete") {
6367 e.preventDefault();
6368 const disabled = this.disabled !== null;
6369 if (disabled) {
6370 return;
6371 }
6372 const label = this.label ?? "";
6373 this.emit("wpd-chip-dismiss", { label });
6374 }
6375 };
6376 }
6377 connectedCallback() {
6378 super.connectedCallback();
6379 this.addEventListener("keydown", this._onHostKeyDown);
6380 }
6381 disconnectedCallback() {
6382 this.removeEventListener("keydown", this._onHostKeyDown);
6383 }
6384 render() {
6385 const label = this.label ?? "";
6386 const dismissible = this.dismissible !== null;
6387 const disabled = this.disabled !== null;
6388 return html`
6389 <span part="chip" class="wpd-chip">
6390 <span class="wpd-chip__icon">
6391 <slot name="icon"></slot>
6392 </span>
6393 <span class="wpd-chip__label">
6394 ${label === "" ? html`<slot></slot>` : label}
6395 </span>
6396 ${dismissible ? html`
6397 <button
6398 part="dismiss"
6399 class="wpd-chip__dismiss"
6400 type="button"
6401 aria-label=${`Remove ${label || "chip"}`}
6402 ?disabled=${disabled}
6403 @click=${(e) => this._onDismiss(e)}
6404 >
6405 ${_iconCross()}
6406 </button>
6407 ` : html``}
6408 </span>
6409 `;
6410 }
6411 _onDismiss(e) {
6412 e.stopPropagation();
6413 const disabled = this.disabled !== null;
6414 if (disabled) {
6415 return;
6416 }
6417 const label = this.label ?? "";
6418 this.emit("wpd-chip-dismiss", { label });
6419 }
6420 };
6421 _WpdChip.props = [
6422 "label",
6423 "tone",
6424 "size",
6425 "dismissible",
6426 "disabled",
6427 "pending"
6428 ];
6429 _WpdChip.styles = [styles$7];
6430 _WpdChip.help = {
6431 title: "Chip",
6432 summary: "Labelled pill primitive with optional leading icon and trailing dismiss button. Tones mirror <wpd-badge>; pair with <wpd-tag-input> for full add/remove ergonomics.",
6433 status: "experimental",
6434 since: "0.8.0",
6435 props: [
6436 {
6437 name: "label",
6438 type: "string",
6439 description: "Visible text. Falls back to the default slot when omitted."
6440 },
6441 {
6442 name: "tone",
6443 type: "'neutral' | 'accent' | 'positive' | 'warning' | 'danger'",
6444 default: "neutral",
6445 description: "Color variant. Mirrors <wpd-badge> tones."
6446 },
6447 {
6448 name: "size",
6449 type: "'default' | 'compact'",
6450 default: "default",
6451 description: "Vertical density. Compact halves horizontal padding for dense lists."
6452 },
6453 {
6454 name: "dismissible",
6455 type: "boolean attribute",
6456 description: "Renders a trailing × button. Click / Enter / Space emits wpd-chip-dismiss."
6457 },
6458 {
6459 name: "disabled",
6460 type: "boolean attribute",
6461 description: "Visually mutes the chip and blocks the dismiss button. Useful while a parent is mid-update."
6462 },
6463 {
6464 name: "pending",
6465 type: "boolean attribute",
6466 description: "Renders a subtle pulse animation while a REST mutation is in flight. Auto-applied by <wpd-tag-input>; safe to set by hand."
6467 }
6468 ],
6469 slots: [
6470 { name: "(default)", description: "Fallback label when `label` is unset." },
6471 {
6472 name: "icon",
6473 description: "Leading icon (Dashicon, SVG, image). Inherits text color."
6474 }
6475 ],
6476 parts: [
6477 { name: "chip", description: "The pill container." },
6478 {
6479 name: "dismiss",
6480 description: "The trailing × button (when `dismissible`)."
6481 }
6482 ],
6483 events: [
6484 {
6485 name: "wpd-chip-dismiss",
6486 description: "Fires when the dismiss button is activated. Detail carries the chip's label so a delegated listener can act without DOM walking.",
6487 detail: "{ label: string }"
6488 }
6489 ],
6490 cssProps: [
6491 { name: "--wpd-chip-bg", description: "Background color." },
6492 { name: "--wpd-chip-fg", description: "Text color." },
6493 { name: "--wpd-chip-border", description: "Border shorthand." },
6494 {
6495 name: "--wpd-chip-padding",
6496 description: "Padding shorthand.",
6497 default: "2px 8px"
6498 },
6499 {
6500 name: "--wpd-chip-radius",
6501 description: "Corner radius.",
6502 default: "999px"
6503 },
6504 {
6505 name: "--wpd-chip-label-max",
6506 description: "Max width of the inner label before ellipsis.",
6507 default: "220px"
6508 }
6509 ],
6510 example: html`
6511 <wpd-cluster gap="6">
6512 <wpd-chip label="Neutral"></wpd-chip>
6513 <wpd-chip label="Accent" tone="accent"></wpd-chip>
6514 <wpd-chip label="Positive" tone="positive"></wpd-chip>
6515 <wpd-chip label="Warning" tone="warning"></wpd-chip>
6516 <wpd-chip label="Danger" tone="danger"></wpd-chip>
6517 <wpd-chip label="Dismissible" dismissible></wpd-chip>
6518 </wpd-cluster>
6519 `
6520 };
6521 let WpdChip = _WpdChip;
6522 defineComponent("wpd-chip", WpdChip);
6523 function _iconCross() {
6524 return html`
6525 <svg
6526 viewBox="0 0 12 12"
6527 width="10"
6528 height="10"
6529 aria-hidden="true"
6530 focusable="false"
6531 fill="none"
6532 stroke="currentColor"
6533 stroke-width="1.5"
6534 stroke-linecap="round"
6535 >
6536 <path d="M3 3 L9 9 M9 3 L3 9" />
6537 </svg>
6538 `;
6539 }
6540 const styles$6 = css`:host{display:flex;flex-direction:row;flex-wrap:wrap;gap:var( --wpd-cluster-gap,8px );justify-content:var( --wpd-cluster-justify,flex-start );align-items:var( --wpd-cluster-align,center )}:host( [ hidden ] ){display:none}`;
6541 const _WpdCluster = class _WpdCluster extends Component {
6542 render() {
6543 const gap = this.gap;
6544 const justify = this.justify;
6545 const align = this.align;
6546 const gapPx = gap && /^\d+$/.test(gap) ? `${gap}px` : "";
6547 if (gapPx) {
6548 this.style.setProperty("--wpd-cluster-gap", gapPx);
6549 }
6550 if (justify) {
6551 this.style.setProperty("--wpd-cluster-justify", justify);
6552 }
6553 if (align) {
6554 this.style.setProperty("--wpd-cluster-align", align);
6555 }
6556 return html`<slot></slot>`;
6557 }
6558 };
6559 _WpdCluster.props = ["gap", "justify", "align"];
6560 _WpdCluster.styles = [styles$6];
6561 _WpdCluster.help = {
6562 title: "Cluster",
6563 summary: "Horizontal flex layout with a gap + wrap. The sibling of <wpd-stack> — use it for rows of controls (button groups, toolbars). Children wrap gracefully when the container narrows.",
6564 status: "stable",
6565 since: "0.5.0",
6566 props: [
6567 {
6568 name: "gap",
6569 type: "integer (px)",
6570 default: "8",
6571 description: "Space between children."
6572 },
6573 {
6574 name: "justify",
6575 type: "'start' | 'center' | 'end' | 'space-between' | 'space-around'",
6576 default: "start",
6577 description: "Main-axis alignment (justify-content)."
6578 },
6579 {
6580 name: "align",
6581 type: "'start' | 'center' | 'end' | 'stretch' | 'baseline'",
6582 default: "center",
6583 description: "Cross-axis alignment (align-items)."
6584 }
6585 ],
6586 slots: [
6587 { name: "(default)", description: "Inline children." }
6588 ],
6589 cssProps: [
6590 { name: "--wpd-cluster-gap", default: "8px" },
6591 { name: "--wpd-cluster-justify", default: "start" },
6592 { name: "--wpd-cluster-align", default: "center" }
6593 ],
6594 example: html`
6595 <wpd-cluster gap="8" justify="end">
6596 <wpd-button>Cancel</wpd-button>
6597 <wpd-button variant="primary">Save</wpd-button>
6598 </wpd-cluster>
6599 `
6600 };
6601 let WpdCluster = _WpdCluster;
6602 defineComponent("wpd-cluster", WpdCluster);
6603 const styles$5 = css`:host{display:flex;flex-direction:column;gap:var( --wpd-stack-gap,12px );align-items:var( --wpd-stack-align,stretch );padding:var( --wpd-stack-padding,0 )}:host( [ hidden ] ){display:none}`;
6604 const _WpdStack = class _WpdStack extends Component {
6605 render() {
6606 const gap = this.gap;
6607 const align = this.align;
6608 const padding = this.padding;
6609 const gapPx = gap && /^\d+$/.test(gap) ? `${gap}px` : "";
6610 if (gapPx) {
6611 this.style.setProperty("--wpd-stack-gap", gapPx);
6612 }
6613 if (align) {
6614 this.style.setProperty("--wpd-stack-align", align);
6615 }
6616 if (padding !== null && /^\d+$/.test(padding)) {
6617 this.style.setProperty("--wpd-stack-padding", `${padding}px`);
6618 }
6619 return html`<slot></slot>`;
6620 }
6621 };
6622 _WpdStack.props = ["gap", "align", "padding"];
6623 _WpdStack.styles = [styles$5];
6624 _WpdStack.help = {
6625 title: "Stack",
6626 summary: 'Vertical flex layout with a gap — the "stack" primitive every design system eventually invents. Use it instead of hand-rolling display:flex; flex-direction:column.',
6627 status: "stable",
6628 since: "0.5.0",
6629 props: [
6630 {
6631 name: "gap",
6632 type: "integer (px)",
6633 default: "12",
6634 description: "Space between children."
6635 },
6636 {
6637 name: "align",
6638 type: "'start' | 'center' | 'end' | 'stretch'",
6639 default: "stretch",
6640 description: "Cross-axis alignment (align-items)."
6641 },
6642 {
6643 name: "padding",
6644 type: "integer (px)",
6645 default: "0",
6646 description: "Inset padding on every side. Pass 0 for edge-to-edge."
6647 }
6648 ],
6649 slots: [
6650 { name: "(default)", description: "Stacked children." }
6651 ],
6652 cssProps: [
6653 { name: "--wpd-stack-gap", default: "12px" },
6654 { name: "--wpd-stack-align", default: "stretch" },
6655 { name: "--wpd-stack-padding", default: "0" }
6656 ],
6657 example: html`
6658 <wpd-stack gap="12">
6659 <wpd-section heading="Foo">First</wpd-section>
6660 <wpd-section heading="Bar">Second</wpd-section>
6661 </wpd-stack>
6662 `
6663 };
6664 let WpdStack = _WpdStack;
6665 defineComponent("wpd-stack", WpdStack);
6666 const styles$4 = css`:host{display:grid;grid-template-columns:var( --wpd-grid-columns,1fr );grid-template-rows:var( --wpd-grid-rows,auto );gap:var( --wpd-grid-gap,8px );column-gap:var( --wpd-grid-column-gap,var( --wpd-grid-gap,8px ) );row-gap:var( --wpd-grid-row-gap,var( --wpd-grid-gap,8px ) )}:host( [ hidden ] ){display:none}`;
6667 const _WpdGrid = class _WpdGrid extends Component {
6668 render() {
6669 const columns = this.columns;
6670 const rows = this.rows;
6671 const gap = this.gap;
6672 const cg = this["column-gap"];
6673 const rg = this["row-gap"];
6674 if (columns && /^\d+$/.test(columns)) {
6675 this.style.setProperty(
6676 "--wpd-grid-columns",
6677 `repeat(${columns}, minmax(0, 1fr))`
6678 );
6679 }
6680 if (rows && /^\d+$/.test(rows)) {
6681 this.style.setProperty(
6682 "--wpd-grid-rows",
6683 `repeat(${rows}, minmax(0, 1fr))`
6684 );
6685 }
6686 if (gap && /^\d+$/.test(gap)) {
6687 this.style.setProperty("--wpd-grid-gap", `${gap}px`);
6688 }
6689 if (cg && /^\d+$/.test(cg)) {
6690 this.style.setProperty("--wpd-grid-column-gap", `${cg}px`);
6691 }
6692 if (rg && /^\d+$/.test(rg)) {
6693 this.style.setProperty("--wpd-grid-row-gap", `${rg}px`);
6694 }
6695 return html`<slot></slot>`;
6696 }
6697 };
6698 _WpdGrid.props = ["columns", "rows", "gap", "column-gap", "row-gap"];
6699 _WpdGrid.styles = [styles$4];
6700 _WpdGrid.help = {
6701 title: "Grid",
6702 summary: 'Neutral CSS grid container. The 2-D twin of <wpd-stack>/<wpd-cluster>. No role is emitted — callers wrap in role="grid"/"radiogroup" if warranted.',
6703 status: "stable",
6704 since: "0.5.0",
6705 props: [
6706 {
6707 name: "columns",
6708 type: "integer",
6709 default: "1",
6710 description: "Number of equal-width columns (repeat(N, minmax(0, 1fr)))."
6711 },
6712 {
6713 name: "rows",
6714 type: "integer",
6715 description: "Optional fixed row count. Omit for content-driven sizing."
6716 },
6717 { name: "gap", type: "integer (px)", description: "Cell spacing on both axes." },
6718 { name: "column-gap", type: "integer (px)", description: "x-axis override." },
6719 { name: "row-gap", type: "integer (px)", description: "y-axis override." }
6720 ],
6721 slots: [
6722 { name: "(default)", description: "Grid children." }
6723 ],
6724 cssProps: [
6725 { name: "--wpd-grid-columns" },
6726 { name: "--wpd-grid-rows" },
6727 { name: "--wpd-grid-gap" },
6728 { name: "--wpd-grid-column-gap" },
6729 { name: "--wpd-grid-row-gap" }
6730 ],
6731 example: html`
6732 <wpd-grid columns="4" gap="8">
6733 <wpd-button>7</wpd-button>
6734 <wpd-button>8</wpd-button>
6735 <wpd-button>9</wpd-button>
6736 <wpd-button variant="primary">÷</wpd-button>
6737 <wpd-button>4</wpd-button>
6738 <wpd-button>5</wpd-button>
6739 <wpd-button>6</wpd-button>
6740 <wpd-button variant="primary">×</wpd-button>
6741 </wpd-grid>
6742 `
6743 };
6744 let WpdGrid = _WpdGrid;
6745 defineComponent("wpd-grid", WpdGrid);
6746 const styles$3 = 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}}`;
6747 const WPD_SPINNER_PRESETS = Object.freeze({
6748 classic: {
6749 sp1: 12,
6750 sp2: 24,
6751 sp3: 40,
6752 a1: 28,
6753 a2: 15,
6754 a3: 8,
6755 gap: 4,
6756 dir2: 1,
6757 dir3: -1,
6758 pulse: "none",
6759 dots: 0
6760 },
6761 comet: {
6762 sp1: 8,
6763 sp2: 14,
6764 sp3: 26,
6765 a1: 50,
6766 a2: 28,
6767 a3: 12,
6768 gap: 3,
6769 dir2: 1,
6770 dir3: 1,
6771 pulse: "none",
6772 dots: 5
6773 },
6774 orbit: {
6775 sp1: 10,
6776 sp2: 10,
6777 sp3: 32,
6778 a1: 50,
6779 a2: 50,
6780 a3: 8,
6781 gap: 5,
6782 dir2: -1,
6783 dir3: -1,
6784 pulse: "opacity",
6785 dots: 3
6786 },
6787 pulse: {
6788 sp1: 6,
6789 sp2: 18,
6790 sp3: 30,
6791 a1: 20,
6792 a2: 12,
6793 a3: 6,
6794 gap: 4,
6795 dir2: 1,
6796 dir3: -1,
6797 pulse: "both",
6798 dots: 8
6799 }
6800 });
6801 const CX = 61.26;
6802 const CY = 61.26;
6803 const DISC_R = 58.453;
6804 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"/>';
6805 const _WpdSpinner = class _WpdSpinner extends Component {
6806 constructor() {
6807 super(...arguments);
6808 this._paintScheduled = false;
6809 }
6810 connectedCallback() {
6811 super.connectedCallback();
6812 this._schedulePaint();
6813 }
6814 render() {
6815 return html`<div class="root" part="root"></div>`;
6816 }
6817 requestUpdate() {
6818 super.requestUpdate();
6819 this._schedulePaint();
6820 }
6821 _schedulePaint() {
6822 if (this._paintScheduled || !this.isConnected) {
6823 return;
6824 }
6825 this._paintScheduled = true;
6826 queueMicrotask(() => {
6827 this._paintScheduled = false;
6828 if (!this.isConnected) {
6829 return;
6830 }
6831 this._paint();
6832 });
6833 }
6834 _paint() {
6835 this._syncCssVars();
6836 const root = this.shadowRoot?.querySelector(
6837 ".root"
6838 );
6839 if (!root) {
6840 return;
6841 }
6842 root.innerHTML = this._buildSvg();
6843 }
6844 /**
6845 * Reflect the color / accent / size attributes onto CSS custom
6846 * properties on the host. Removing the attribute clears the var
6847 * so the default cascades back in.
6848 */
6849 _syncCssVars() {
6850 const sync = (attr, varName, transform) => {
6851 const v = this.getAttribute(attr);
6852 if (v === null) {
6853 this.style.removeProperty(varName);
6854 } else {
6855 this.style.setProperty(
6856 varName,
6857 transform ? transform(v) : v
6858 );
6859 }
6860 };
6861 sync("color", "--wpd-spinner-color");
6862 sync("accent", "--wpd-spinner-accent");
6863 sync(
6864 "size",
6865 "--wpd-spinner-size",
6866 (v) => /^-?\d+(\.\d+)?$/.test(v.trim()) ? `${v}px` : v
6867 );
6868 }
6869 _effectiveConfig() {
6870 const presetName = this.getAttribute("preset") ?? "classic";
6871 const preset = WPD_SPINNER_PRESETS[presetName] ?? WPD_SPINNER_PRESETS.classic;
6872 const num = (attr, fallback) => {
6873 const v = this.getAttribute(attr);
6874 if (v === null) {
6875 return fallback;
6876 }
6877 const n = parseFloat(v);
6878 return Number.isFinite(n) ? n : fallback;
6879 };
6880 const dir = (attr, fallback) => {
6881 const v = this.getAttribute(attr);
6882 if (v === null) {
6883 return fallback;
6884 }
6885 const lc = v.toLowerCase();
6886 if (lc === "-1" || lc === "ccw" || lc === "reverse") {
6887 return -1;
6888 }
6889 return 1;
6890 };
6891 const pulse = () => {
6892 const v = this.getAttribute("pulse");
6893 if (v === "scale" || v === "opacity" || v === "both" || v === "none") {
6894 return v;
6895 }
6896 return preset.pulse;
6897 };
6898 return {
6899 sp1: num("sp1", preset.sp1),
6900 sp2: num("sp2", preset.sp2),
6901 sp3: num("sp3", preset.sp3),
6902 a1: num("a1", preset.a1),
6903 a2: num("a2", preset.a2),
6904 a3: num("a3", preset.a3),
6905 gap: num("gap", preset.gap),
6906 dir2: dir("dir2", preset.dir2),
6907 dir3: dir("dir3", preset.dir3),
6908 pulse: pulse(),
6909 dots: Math.max(0, Math.floor(num("dots", preset.dots)))
6910 };
6911 }
6912 _buildSvg() {
6913 const cfg = this._effectiveConfig();
6914 const label = escAttr(this.getAttribute("label") ?? "Loading");
6915 const pad = cfg.gap * 3 + 14;
6916 const vbMin = -pad;
6917 const vbSize = 122.52 + pad * 2;
6918 const r1 = DISC_R + cfg.gap + 2;
6919 const r2 = r1 + cfg.gap + 2;
6920 const r3 = r2 + cfg.gap + 1.5;
6921 const ring1Anim = `animation: wpd-spinner-spin ${(cfg.sp1 / 10).toFixed(2)}s linear infinite`;
6922 const ring2Anim = `animation: wpd-spinner-spin ${(cfg.sp2 / 10).toFixed(2)}s linear infinite${cfg.dir2 < 0 ? " reverse" : ""}`;
6923 const ring3Anim = `animation: wpd-spinner-spin ${(cfg.sp3 / 10).toFixed(2)}s linear infinite${cfg.dir3 < 0 ? " reverse" : ""}`;
6924 const pspd = (cfg.sp1 * 1.8 / 10).toFixed(1);
6925 const ospd = (cfg.sp1 * 2.3 / 10).toFixed(1);
6926 let pulseStyle = "";
6927 if (cfg.pulse === "scale") {
6928 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite`;
6929 } else if (cfg.pulse === "opacity") {
6930 pulseStyle = `animation: wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
6931 } else if (cfg.pulse === "both") {
6932 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite, wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
6933 }
6934 let dotEls = "";
6935 if (cfg.dots > 0) {
6936 const dr = r3 + cfg.gap + 1;
6937 const dc2 = 2 * Math.PI * dr;
6938 const dsz = 1.6;
6939 const dotDur = (cfg.sp1 * 0.65 / 10).toFixed(2);
6940 for (let i = 0; i < cfg.dots; i++) {
6941 const offset = -(i / cfg.dots) * dc2;
6942 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"/>`;
6943 }
6944 }
6945 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>`;
6946 }
6947 };
6948 _WpdSpinner.props = [
6949 "preset",
6950 "size",
6951 "color",
6952 "accent",
6953 "sp1",
6954 "sp2",
6955 "sp3",
6956 "a1",
6957 "a2",
6958 "a3",
6959 "gap",
6960 "dir2",
6961 "dir3",
6962 "pulse",
6963 "dots",
6964 "label"
6965 ];
6966 _WpdSpinner.styles = [styles$3];
6967 _WpdSpinner.help = {
6968 title: "Spinner",
6969 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.",
6970 status: "experimental",
6971 since: "0.6.0",
6972 props: [
6973 {
6974 name: "preset",
6975 type: '"classic" | "comet" | "orbit" | "pulse"',
6976 default: "classic",
6977 description: "Visual personality. Every other attribute defaults to the preset's value and can be overridden individually."
6978 },
6979 {
6980 name: "size",
6981 type: "integer (px) or CSS length",
6982 default: "48",
6983 description: "Sets `--wpd-spinner-size`. Bare numbers are treated as px; pass a CSS length (e.g. `2em`) to opt into ems / rems."
6984 },
6985 {
6986 name: "color",
6987 type: "CSS color",
6988 description: "Disc + ring + dot color. Sets `--wpd-spinner-color`. Default inherits the WP admin theme color."
6989 },
6990 {
6991 name: "accent",
6992 type: "CSS color",
6993 default: "#fff",
6994 description: "Color of the W mark inside the disc. Sets `--wpd-spinner-accent`. Default white — change for dark-on-light or themed marks."
6995 },
6996 {
6997 name: "sp1, sp2, sp3",
6998 type: "integer (deciseconds)",
6999 description: "Per-ring rotation duration in tenths-of-a-second (12 → 1.2s). Higher = slower."
7000 },
7001 {
7002 name: "a1, a2, a3",
7003 type: "integer (0-100)",
7004 description: "Per-ring arc length as a percentage of the ring circumference."
7005 },
7006 {
7007 name: "gap",
7008 type: "integer",
7009 description: "Gap between concentric rings (units approximate to px at 120-viewport)."
7010 },
7011 {
7012 name: "dir2, dir3",
7013 type: '"1" | "-1" | "cw" | "ccw"',
7014 description: "Per-ring direction; ring 1 is always clockwise."
7015 },
7016 {
7017 name: "pulse",
7018 type: '"none" | "scale" | "opacity" | "both"',
7019 description: "Pulse animation applied to the disc + W mark."
7020 },
7021 {
7022 name: "dots",
7023 type: "integer",
7024 description: "Outer trailing dot count. Sensible values: 0, 3, 5, 8."
7025 },
7026 {
7027 name: "label",
7028 type: "string",
7029 default: "Loading",
7030 description: 'Accessible name for the SVG (`role="img"` + `aria-label`).'
7031 }
7032 ],
7033 cssProps: [
7034 { name: "--wpd-spinner-color", default: "var(--wp-admin-theme-color, #21759b)" },
7035 { name: "--wpd-spinner-accent", default: "#fff" },
7036 { name: "--wpd-spinner-size", default: "48px" }
7037 ],
7038 example: html`<wpd-spinner preset="comet" size="80"></wpd-spinner>`
7039 };
7040 let WpdSpinner = _WpdSpinner;
7041 function dasharray(r, pct) {
7042 const c = 2 * Math.PI * r;
7043 const visible = pct / 100 * c;
7044 const gap = c - visible;
7045 return `${visible.toFixed(2)} ${gap.toFixed(2)}`;
7046 }
7047 function escAttr(s) {
7048 return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7049 }
7050 defineComponent("wpd-spinner", WpdSpinner);
7051 const styles$2 = css`:host{display:inline-flex;align-items:center;justify-content:center;width:var( --wpd-icon-size,16px );height:var( --wpd-icon-size,16px );color:inherit;line-height:1}:host( [ hidden ] ){display:none}.wpd-icon__glyph{font-size:var( --wpd-icon-size,16px );width:var( --wpd-icon-size,16px );height:var( --wpd-icon-size,16px );line-height:1;color:inherit;display:inline-flex;align-items:center;justify-content:center}.wpd-icon__glyph--char{font-family:dashicons;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;speak:none}.wpd-icon__glyph.dashicons{font-family:dashicons}`;
7052 let _cache = null;
7053 function parseCssContentToChar(raw) {
7054 let value = raw.trim();
7055 if (value === "") {
7056 return null;
7057 }
7058 if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
7059 value = value.slice(1, -1);
7060 }
7061 const escaped = value.match(/^\\([0-9a-f]{1,6})\s?$/i);
7062 if (escaped) {
7063 return String.fromCodePoint(parseInt(escaped[1], 16));
7064 }
7065 return value || null;
7066 }
7067 function buildMap() {
7068 const map = /* @__PURE__ */ new Map();
7069 if (typeof document === "undefined") {
7070 return map;
7071 }
7072 const sheets = Array.from(document.styleSheets ?? []);
7073 for (const sheet of sheets) {
7074 let rules = null;
7075 try {
7076 rules = sheet.cssRules;
7077 } catch {
7078 continue;
7079 }
7080 if (!rules) {
7081 continue;
7082 }
7083 for (const rule of Array.from(rules)) {
7084 const styleRule = rule;
7085 if (!styleRule || !styleRule.selectorText) {
7086 continue;
7087 }
7088 const match = styleRule.selectorText.match(
7089 /\.dashicons-([a-z0-9-]+)::?before/i
7090 );
7091 if (!match) {
7092 continue;
7093 }
7094 const content = styleRule.style?.content;
7095 if (!content) {
7096 continue;
7097 }
7098 const char = parseCssContentToChar(content);
7099 if (char) {
7100 map.set(match[1], char);
7101 }
7102 }
7103 }
7104 return map;
7105 }
7106 function resolveDashicon(name) {
7107 if (!_cache) {
7108 _cache = buildMap();
7109 }
7110 const slug = name.startsWith("dashicons-") ? name.slice("dashicons-".length) : name;
7111 return _cache.get(slug) ?? null;
7112 }
7113 function refreshDashiconCache() {
7114 _cache = buildMap();
7115 }
7116 let _scheduled = false;
7117 function primeOnLoad() {
7118 if (_scheduled || typeof window === "undefined") {
7119 return;
7120 }
7121 _scheduled = true;
7122 const refresh = () => {
7123 refreshDashiconCache();
7124 };
7125 if (document.readyState === "loading") {
7126 document.addEventListener("DOMContentLoaded", refresh, { once: true });
7127 }
7128 window.addEventListener("load", refresh, { once: true });
7129 }
7130 primeOnLoad();
7131 const _WpdIcon = class _WpdIcon extends Component {
7132 render() {
7133 const rawName = this.name || "";
7134 const slug = rawName.startsWith("dashicons-") ? rawName.slice("dashicons-".length) : rawName;
7135 const size = this.size;
7136 if (size && /^\d+$/.test(size)) {
7137 this.style.setProperty("--wpd-icon-size", `${size}px`);
7138 }
7139 const char = resolveDashicon(slug);
7140 if (char) {
7141 return html`<span
7142 class="wpd-icon__glyph wpd-icon__glyph--char dashicons dashicons-${slug}"
7143 aria-hidden="true"
7144 >${char}</span>`;
7145 }
7146 return html`<span
7147 class="wpd-icon__glyph dashicons dashicons-${slug}"
7148 aria-hidden="true"
7149 ></span>`;
7150 }
7151 };
7152 _WpdIcon.props = ["name", "size"];
7153 _WpdIcon.styles = [styles$2];
7154 _WpdIcon.help = {
7155 title: "Icon",
7156 summary: 'Dashicon wrapper that inherits theme colour + sizing from its context. Accepts either the dashicon suffix ("calculator") or the full class ("dashicons-calculator"). Marked aria-hidden; wrap in a button/link with its own label for accessible use.',
7157 status: "stable",
7158 since: "0.5.0",
7159 props: [
7160 {
7161 name: "name",
7162 type: "string",
7163 description: "Dashicon identifier, with or without the `dashicons-` prefix."
7164 },
7165 {
7166 name: "size",
7167 type: "integer (px)",
7168 default: "16",
7169 description: "Glyph size in pixels."
7170 }
7171 ],
7172 cssProps: [
7173 { name: "--wpd-icon-size", default: "16px" }
7174 ],
7175 example: html`
7176 <wpd-cluster gap="8" align="center">
7177 <wpd-icon name="admin-post"></wpd-icon>
7178 <wpd-icon name="calculator" size="20"></wpd-icon>
7179 <wpd-icon name="dashicons-star-filled" size="32"></wpd-icon>
7180 </wpd-cluster>
7181 `
7182 };
7183 let WpdIcon = _WpdIcon;
7184 defineComponent("wpd-icon", WpdIcon);
7185 const styles$1 = css`:host{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:32px 24px;text-align:center;color:var( --wpd-empty-state-fg,var( --desktop-mode-muted,#646970 ) )}:host( [ hidden ] ){display:none}.wpd-empty-state__icon{margin-bottom:4px;color:var( --wpd-empty-state-icon-color,currentColor );opacity:0.75}.wpd-empty-state__heading{margin:0;font-size:14px;font-weight:600;color:var( --desktop-mode-text,#1d2327 )}.wpd-empty-state__description{margin:0;font-size:12px;line-height:1.4;max-width:48ch}.wpd-empty-state__description:empty{display:none}.wpd-empty-state__cta{margin-top:8px}.wpd-empty-state__cta:empty{display:none}`;
7186 const _WpdEmptyState = class _WpdEmptyState extends Component {
7187 render() {
7188 const icon = this.icon || "";
7189 const heading = this.heading || "";
7190 const description = this.description || "";
7191 return html`
7192 ${icon ? html`<wpd-icon
7193 class="wpd-empty-state__icon"
7194 name=${icon}
7195 size="28"
7196 ></wpd-icon>` : null}
7197 <h3 class="wpd-empty-state__heading">${heading}</h3>
7198 <p class="wpd-empty-state__description">${description}</p>
7199 <div class="wpd-empty-state__cta">
7200 <slot name="cta"></slot>
7201 </div>
7202 <slot></slot>
7203 `;
7204 }
7205 };
7206 _WpdEmptyState.props = ["icon", "heading", "description"];
7207 _WpdEmptyState.styles = [styles$1];
7208 _WpdEmptyState.help = {
7209 title: "Empty state",
7210 summary: 'Centered placeholder for "nothing here yet" UI: icon + heading + description + optional CTA. A canonical shape so empty states look consistent across the shell.',
7211 status: "stable",
7212 since: "0.5.0",
7213 props: [
7214 {
7215 name: "icon",
7216 type: "string (dashicons slug)",
7217 description: "Dashicons identifier (with or without the dashicons- prefix)."
7218 },
7219 {
7220 name: "heading",
7221 type: "string",
7222 description: "Bold first line."
7223 },
7224 {
7225 name: "description",
7226 type: "string",
7227 description: "Secondary paragraph below the heading."
7228 }
7229 ],
7230 slots: [
7231 { name: "cta", description: "Call-to-action button row below the description." },
7232 { name: "(default)", description: "Any additional content rendered after the CTA." }
7233 ],
7234 cssProps: [
7235 { name: "--desktop-mode-text", description: "Heading colour." },
7236 { name: "--desktop-mode-muted", description: "Description colour." },
7237 { name: "--wpd-empty-state-fg" },
7238 { name: "--wpd-empty-state-icon-color" }
7239 ],
7240 example: html`
7241 <wpd-empty-state
7242 icon="admin-plugins"
7243 heading="No plugins installed yet"
7244 description="Install a plugin to see it here."
7245 >
7246 <wpd-button slot="cta" variant="primary">Browse plugins</wpd-button>
7247 </wpd-empty-state>
7248 `
7249 };
7250 let WpdEmptyState = _WpdEmptyState;
7251 defineComponent("wpd-empty-state", WpdEmptyState);
7252 const styles = css`:host{display:block;--_track:var( --wpd-rating-track,rgba( 0,0,0,0.08 ) );--_fill:var( --wpd-rating-fill,linear-gradient( 90deg,#f5af00 0%,#ffd245 100% ) );--_star:var( --wpd-rating-star,#f5af00 );--_star-empty:var( --wpd-rating-star-empty,rgba( 0,0,0,0.18 ) );--_surface:var( --wpd-rating-surface,var( --wpd-surface-raised,rgba( 255,255,255,0.7 ) ) );--_border:var( --wpd-rating-border,var( --wpd-border,rgba( 0,0,0,0.08 ) ) );--_fg:var( --wpd-rating-fg,var( --wpd-fg,inherit ) );--_fg-muted:var( --wpd-rating-fg-muted,var( --wpd-fg-muted,#666 ) )}:host( [ hidden ] ){display:none}.summary-card{display:grid;grid-template-columns:minmax( 140px,180px ) 1fr;gap:28px;align-items:center;padding:18px 20px;background:var( --_surface );border:1px solid var( --_border );border-radius:14px;color:var( --_fg )}.summary{display:flex;flex-direction:column;align-items:center;gap:8px;padding-inline-end:20px;border-inline-end:1px solid var( --_border )}.big{font-size:44px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;letter-spacing:-0.02em;color:var( --_fg )}.stars{display:inline-flex;gap:2px;color:var( --_star )}.stars svg{width:16px;height:16px;display:block}.stars .empty{color:var( --_star-empty )}.total{font-size:12px;color:var( --_fg-muted )}.bars{display:grid;gap:6px;min-width:0}.row{display:grid;grid-template-columns:42px 1fr 60px;gap:12px;align-items:center;font-size:12.5px;color:var( --_fg-muted )}.row__label{display:inline-flex;align-items:center;gap:4px;font-variant-numeric:tabular-nums;font-weight:600}.row__label svg{width:11px;height:11px;color:var( --_star )}.row__track{position:relative;height:10px;background:var( --_track );border-radius:999px;overflow:hidden}.row__fill{position:absolute;inset:0;background:var( --_fill );border-radius:999px;transform-origin:left center;transform:scaleX( var( --ratio,0 ) );transition:transform 600ms cubic-bezier( 0.2,0.8,0.2,1 )}:host(:dir( rtl ) ) .row__fill,:host-context( [ dir='rtl' ] ) .row__fill{transform-origin:right center}.row__count{text-align:end;font-variant-numeric:tabular-nums;color:var( --_fg )}@media ( prefers-reduced-motion:reduce ){.row__fill{transition:none}}@media ( max-width:540px ){.summary-card{grid-template-columns:1fr;gap:16px}.summary{padding-inline-end:0;border-inline-end:0;border-block-end:1px solid var( --_border );padding-block-end:14px}}`;
7253 const _WpdRatingSummary = class _WpdRatingSummary extends Component {
7254 constructor() {
7255 super(...arguments);
7256 this._ratings = {};
7257 }
7258 /**
7259 * Per-star counts. Setting this triggers a re-render so consumers
7260 * can swap data without recreating the element.
7261 */
7262 get ratings() {
7263 return { ...this._ratings };
7264 }
7265 set ratings(next) {
7266 this._ratings = next ? { ...next } : {};
7267 this.requestUpdate();
7268 }
7269 render() {
7270 const rating = clamp01to100(numAttr(this, "rating"));
7271 const stars0to5 = rating / 100 * 5;
7272 const totalAttr = numAttr(this, "total");
7273 const total = totalAttr > 0 ? totalAttr : (this._ratings["5"] ?? 0) + (this._ratings["4"] ?? 0) + (this._ratings["3"] ?? 0) + (this._ratings["2"] ?? 0) + (this._ratings["1"] ?? 0);
7274 const fmt = new Intl.NumberFormat();
7275 const big = rating > 0 ? (rating / 100 * 5).toFixed(1) : "";
7276 return html`
7277 <div class="summary-card" role="img" aria-label=${ariaLabel(rating, total)}>
7278 <div class="summary">
7279 <div class="big">${big}</div>
7280 <div class="stars" aria-hidden="true">
7281 ${renderStarRow(stars0to5)}
7282 </div>
7283 <div class="total">
7284 ${total === 1 ? "1 rating" : `${fmt.format(total)} ratings`}
7285 </div>
7286 </div>
7287 <div class="bars">
7288 ${[5, 4, 3, 2, 1].map((star) => {
7289 const count = this._ratings[String(star)] ?? 0;
7290 const ratio = total === 0 ? 0 : count / total;
7291 return html`
7292 <div
7293 class="row"
7294 role="presentation"
7295 aria-label=${`${star} stars: ${fmt.format(count)}`}
7296 >
7297 <span class="row__label">
7298 ${star} ${filledStarSvg()}
7299 </span>
7300 <span class="row__track">
7301 <span
7302 class="row__fill"
7303 style=${`--ratio: ${ratio.toFixed(4)}`}
7304 ></span>
7305 </span>
7306 <span class="row__count">${fmt.format(count)}</span>
7307 </div>
7308 `;
7309 })}
7310 </div>
7311 </div>
7312 `;
7313 }
7314 };
7315 _WpdRatingSummary.props = ["rating", "total"];
7316 _WpdRatingSummary.styles = [styles];
7317 _WpdRatingSummary.help = {
7318 title: "Rating summary",
7319 summary: "Two-pane rating distribution: big average + 5-star cluster + total count on the left, one animated bar per star bucket on the right. Mirrors the WordPress.org plugin reviews summary.",
7320 status: "experimental",
7321 since: "0.8.5",
7322 props: [
7323 {
7324 name: "rating",
7325 type: "number (0–100)",
7326 description: "Average rating on the wp.org 0–100 scale. Converted to a 0–5 display inside."
7327 },
7328 {
7329 name: "total",
7330 type: "number",
7331 description: "Total number of ratings. Optional — auto-summed from `ratings` when omitted."
7332 }
7333 ],
7334 cssProps: [
7335 { name: "--wpd-rating-fill", description: "Background of the bar fill." },
7336 { name: "--wpd-rating-track", description: "Background of the empty bar track." },
7337 { name: "--wpd-rating-star", description: "Color of filled stars." },
7338 { name: "--wpd-rating-star-empty", description: "Color of empty stars." },
7339 { name: "--wpd-rating-surface", description: "Card background." },
7340 { name: "--wpd-rating-border", description: "Card border color." },
7341 { name: "--wpd-rating-fg", description: "Primary text color." },
7342 { name: "--wpd-rating-fg-muted", description: "Secondary text color." }
7343 ],
7344 example: html`
7345 <wpd-rating-summary rating="92"></wpd-rating-summary>
7346 `
7347 };
7348 let WpdRatingSummary = _WpdRatingSummary;
7349 function numAttr(host, name) {
7350 const raw = host.getAttribute(name);
7351 if (raw === null || raw === "") {
7352 return 0;
7353 }
7354 const n = Number(raw);
7355 return Number.isFinite(n) ? n : 0;
7356 }
7357 function clamp01to100(n) {
7358 if (n < 0) {
7359 return 0;
7360 }
7361 if (n > 100) {
7362 return 100;
7363 }
7364 return n;
7365 }
7366 function ariaLabel(rating, total) {
7367 if (total === 0) {
7368 return "No ratings yet";
7369 }
7370 const stars = rating / 100 * 5;
7371 return `Average rating ${stars.toFixed(1)} out of 5, from ${total} ratings`;
7372 }
7373 function renderStarRow(stars0to5) {
7374 const full = Math.floor(stars0to5);
7375 const half = stars0to5 - full >= 0.5 ? 1 : 0;
7376 const empty = 5 - full - half;
7377 const list = [];
7378 for (let i = 0; i < full; i++) {
7379 list.push(filledStarSvg());
7380 }
7381 for (let i = 0; i < half; i++) {
7382 list.push(halfStarSvg());
7383 }
7384 for (let i = 0; i < empty; i++) {
7385 list.push(emptyStarSvg());
7386 }
7387 return list;
7388 }
7389 function filledStarSvg() {
7390 return html`
7391 <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
7392 <path
7393 d="M8 1.5l2.06 4.17 4.6.67-3.33 3.25.79 4.58L8 12L3.88 14.17l.79-4.58L1.34 6.34l4.6-.67L8 1.5z"
7394 />
7395 </svg>
7396 `;
7397 }
7398 function halfStarSvg() {
7399 return html`
7400 <svg viewBox="0 0 16 16" aria-hidden="true">
7401 <defs>
7402 <linearGradient id="wpd-half-star">
7403 <stop offset="50%" stop-color="currentColor" />
7404 <stop
7405 offset="50%"
7406 stop-color="currentColor"
7407 stop-opacity="0.22"
7408 />
7409 </linearGradient>
7410 </defs>
7411 <path
7412 fill="url(#wpd-half-star)"
7413 d="M8 1.5l2.06 4.17 4.6.67-3.33 3.25.79 4.58L8 12L3.88 14.17l.79-4.58L1.34 6.34l4.6-.67L8 1.5z"
7414 />
7415 </svg>
7416 `;
7417 }
7418 function emptyStarSvg() {
7419 return html`
7420 <svg
7421 class="empty"
7422 viewBox="0 0 16 16"
7423 fill="currentColor"
7424 aria-hidden="true"
7425 >
7426 <path
7427 d="M8 1.5l2.06 4.17 4.6.67-3.33 3.25.79 4.58L8 12L3.88 14.17l.79-4.58L1.34 6.34l4.6-.67L8 1.5z"
7428 />
7429 </svg>
7430 `;
7431 }
7432 defineComponent("wpd-rating-summary", WpdRatingSummary);
7433 const wpOrgCache = /* @__PURE__ */ new Map();
7434 const reviewsCache = /* @__PURE__ */ new Map();
7435 function buildInstalledDetail(row) {
7436 const root = document.createElement("div");
7437 root.className = "desktop-mode-plugins__detail";
7438 root.setAttribute("data-noclick", "");
7439 const style = document.createElement("style");
7440 style.textContent = PANEL_STYLES;
7441 root.appendChild(style);
7442 const slug = deriveSlug(row);
7443 root.appendChild(buildHero(row));
7444 const tabsHost = document.createElement("div");
7445 tabsHost.className = "desktop-mode-plugins__detail-tabs-wrap";
7446 root.appendChild(tabsHost);
7447 const body = document.createElement("div");
7448 body.className = "desktop-mode-plugins__detail-body";
7449 root.appendChild(body);
7450 const tabs = document.createElement("wpd-tabs");
7451 tabs.className = "desktop-mode-plugins__detail-tabs";
7452 tabs.setAttribute("value", "overview");
7453 const tabDefs = [
7454 { value: "overview", label: __("Overview", "desktop-mode"), show: true },
7455 { value: "details", label: __("Details", "desktop-mode"), show: true },
7456 { value: "changelog", label: __("Changelog", "desktop-mode"), show: !!slug },
7457 { value: "faq", label: __("FAQ", "desktop-mode"), show: !!slug },
7458 { value: "reviews", label: __("Reviews", "desktop-mode"), show: !!slug }
7459 ];
7460 for (const def of tabDefs) {
7461 if (!def.show) {
7462 continue;
7463 }
7464 const tab = document.createElement("wpd-tab");
7465 tab.setAttribute("value", def.value);
7466 tab.textContent = def.label;
7467 tabs.appendChild(tab);
7468 }
7469 tabsHost.appendChild(tabs);
7470 let info = slug ? wpOrgCache.get(slug) ?? null : null;
7471 let infoFetching = false;
7472 let active = "overview";
7473 const ensureInfo = () => {
7474 if (!slug || info || infoFetching) {
7475 return;
7476 }
7477 infoFetching = true;
7478 void (async () => {
7479 try {
7480 info = await fetchPluginInfo(slug);
7481 wpOrgCache.set(slug, info);
7482 if (root.isConnected) {
7483 paintActive();
7484 }
7485 } catch {
7486 if (root.isConnected) {
7487 paintActive();
7488 }
7489 } finally {
7490 infoFetching = false;
7491 }
7492 })();
7493 };
7494 const paintActive = () => {
7495 body.replaceChildren(renderTab(active, row, slug, info));
7496 };
7497 tabs.addEventListener("wpd-tab-change", (ev) => {
7498 const detail = ev.detail;
7499 active = detail?.value ?? "overview";
7500 if (slug && active !== "overview" && active !== "details") {
7501 ensureInfo();
7502 }
7503 paintActive();
7504 });
7505 paintActive();
7506 return root;
7507 }
7508 function buildHero(row, _slug) {
7509 const hero = document.createElement("div");
7510 hero.className = "desktop-mode-plugins__detail-hero";
7511 const inner = document.createElement("div");
7512 inner.className = "desktop-mode-plugins__detail-hero-inner";
7513 const iconTile = document.createElement("div");
7514 iconTile.className = "desktop-mode-plugins__detail-hero-icon";
7515 const iconUrl = row.desktop_mode_icon_url;
7516 if (iconUrl) {
7517 const img = document.createElement("img");
7518 img.alt = "";
7519 img.loading = "lazy";
7520 img.decoding = "async";
7521 img.src = attachIconFallback(img, iconUrl, () => {
7522 iconTile.replaceChildren(buildFallbackGlyph());
7523 });
7524 iconTile.appendChild(img);
7525 } else {
7526 iconTile.appendChild(buildFallbackGlyph());
7527 }
7528 const titleBlock = document.createElement("wpd-stack");
7529 titleBlock.setAttribute("gap", "6");
7530 titleBlock.className = "desktop-mode-plugins__detail-hero-text";
7531 const titleRow = document.createElement("wpd-cluster");
7532 titleRow.setAttribute("gap", "10");
7533 titleRow.setAttribute("align", "center");
7534 const title = document.createElement("h3");
7535 title.className = "desktop-mode-plugins__detail-title";
7536 title.textContent = row.name || row.plugin;
7537 titleRow.appendChild(title);
7538 if (row.version) {
7539 const ver = document.createElement("wpd-badge");
7540 ver.setAttribute("tone", "neutral");
7541 ver.setAttribute("no-dot", "");
7542 ver.textContent = sprintf(
7543 /* translators: %s: version number */
7544 __("v%s", "desktop-mode"),
7545 row.version
7546 );
7547 titleRow.appendChild(ver);
7548 }
7549 const isActive = row.status === "active" || row.status === "active-network";
7550 const statusBadge = document.createElement("wpd-badge");
7551 statusBadge.setAttribute("tone", isActive ? "success" : "neutral");
7552 statusBadge.textContent = isActive ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode");
7553 titleRow.appendChild(statusBadge);
7554 const update = row.desktop_mode_update_available;
7555 if (update?.available && update.new_version) {
7556 const upd = document.createElement("wpd-badge");
7557 upd.setAttribute("tone", "warning");
7558 upd.textContent = sprintf(
7559 /* translators: %s: new version available */
7560 __("Update to %s", "desktop-mode"),
7561 update.new_version
7562 );
7563 titleRow.appendChild(upd);
7564 }
7565 titleBlock.appendChild(titleRow);
7566 const byline = document.createElement("p");
7567 byline.className = "desktop-mode-plugins__detail-byline";
7568 const authorText = stripHtml$1(row.author ?? "") || __("Unknown author", "desktop-mode");
7569 if (row.author_uri) {
7570 const a = document.createElement("a");
7571 a.href = row.author_uri;
7572 a.target = "_blank";
7573 a.rel = "noopener noreferrer";
7574 a.textContent = authorText;
7575 a.setAttribute("data-noclick", "");
7576 byline.append(__("by", "desktop-mode") + " ", a);
7577 } else {
7578 byline.textContent = sprintf(
7579 /* translators: %s: plugin author */
7580 __("by %s", "desktop-mode"),
7581 authorText
7582 );
7583 }
7584 titleBlock.appendChild(byline);
7585 inner.append(iconTile, titleBlock);
7586 hero.appendChild(inner);
7587 return hero;
7588 }
7589 function renderTab(tab, row, slug, info) {
7590 if (tab === "overview") {
7591 return renderOverview(row, slug, info);
7592 }
7593 if (tab === "details") {
7594 return renderDetails(row);
7595 }
7596 if (tab === "changelog") {
7597 return renderChangelog(info);
7598 }
7599 if (tab === "faq") {
7600 return renderFaq(info);
7601 }
7602 return renderReviews(slug, info);
7603 }
7604 function renderOverview(row, slug, info) {
7605 const stack = document.createElement("wpd-stack");
7606 stack.setAttribute("gap", "20");
7607 const chipStrip = buildOverviewChips(row, info);
7608 if (chipStrip.children.length > 0) {
7609 stack.appendChild(chipStrip);
7610 }
7611 const descHtml = info?.sections?.description ?? info?.short_description ?? readDescription(row);
7612 if (descHtml) {
7613 const desc = document.createElement("div");
7614 desc.className = "desktop-mode-plugins__detail-html";
7615 desc.innerHTML = sanitizeHtml(descHtml);
7616 sanitizeLinks(desc);
7617 stack.appendChild(desc);
7618 } else if (slug && !info) {
7619 stack.appendChild(buildLoadingBlock(__("Loading description…", "desktop-mode")));
7620 } else {
7621 stack.appendChild(
7622 buildEmpty(
7623 "admin-plugins",
7624 __("No description", "desktop-mode"),
7625 __("This plugin doesn’t ship a description in its header.", "desktop-mode")
7626 )
7627 );
7628 }
7629 const actions = document.createElement("wpd-cluster");
7630 actions.setAttribute("gap", "8");
7631 actions.className = "desktop-mode-plugins__detail-actions";
7632 if (slug) {
7633 actions.appendChild(
7634 linkButton(
7635 "primary",
7636 __("View on WordPress.org", "desktop-mode"),
7637 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/`
7638 )
7639 );
7640 }
7641 if (row.plugin_uri) {
7642 actions.appendChild(
7643 linkButton("secondary", __("Plugin website", "desktop-mode"), row.plugin_uri)
7644 );
7645 }
7646 if (row.author_uri) {
7647 actions.appendChild(
7648 linkButton("ghost", __("Author website", "desktop-mode"), row.author_uri)
7649 );
7650 }
7651 if (actions.children.length > 0) {
7652 stack.appendChild(actions);
7653 }
7654 return stack;
7655 }
7656 function buildOverviewChips(row, info) {
7657 const strip = document.createElement("wpd-cluster");
7658 strip.setAttribute("gap", "8");
7659 strip.className = "desktop-mode-plugins__detail-chip-strip";
7660 if (info) {
7661 if (typeof info.rating === "number" && info.rating > 0) {
7662 const stars = document.createElement("span");
7663 stars.className = "desktop-mode-plugins__detail-stars-pill";
7664 stars.appendChild(buildStarCluster(info.rating, info.num_ratings ?? 0));
7665 strip.appendChild(stars);
7666 }
7667 if (info.active_installs) {
7668 strip.appendChild(
7669 chip(
7670 "admin-users",
7671 sprintf(
7672 /* translators: %s: comma-grouped active install count */
7673 __("%s+ active installs", "desktop-mode"),
7674 new Intl.NumberFormat().format(info.active_installs)
7675 )
7676 )
7677 );
7678 }
7679 if (info.last_updated) {
7680 strip.appendChild(
7681 chip(
7682 "update",
7683 sprintf(
7684 /* translators: %s: date the plugin was last updated */
7685 __("Updated %s", "desktop-mode"),
7686 humanDate(info.last_updated)
7687 )
7688 )
7689 );
7690 }
7691 if (info.tested) {
7692 strip.appendChild(
7693 chip(
7694 "wordpress-alt",
7695 sprintf(
7696 /* translators: %s: maximum tested WordPress version */
7697 __("Tested up to WP %s", "desktop-mode"),
7698 info.tested
7699 )
7700 )
7701 );
7702 }
7703 }
7704 if (row.requires_wp) {
7705 strip.appendChild(
7706 chip(
7707 "wordpress",
7708 sprintf(
7709 /* translators: %s: minimum WordPress version */
7710 __("Requires WP %s+", "desktop-mode"),
7711 row.requires_wp
7712 )
7713 )
7714 );
7715 }
7716 if (row.requires_php) {
7717 strip.appendChild(
7718 chip(
7719 "editor-code",
7720 sprintf(
7721 /* translators: %s: minimum PHP version */
7722 __("Requires PHP %s+", "desktop-mode"),
7723 row.requires_php
7724 )
7725 )
7726 );
7727 }
7728 if (row.network_only) {
7729 strip.appendChild(
7730 chip("networking", __("Network only", "desktop-mode"))
7731 );
7732 }
7733 return strip;
7734 }
7735 function renderDetails(row) {
7736 const grid = document.createElement("wpd-grid");
7737 grid.setAttribute("columns", "2");
7738 grid.setAttribute("gap", "12");
7739 grid.className = "desktop-mode-plugins__detail-grid";
7740 pushFactCard(grid, "media-document", __("Plugin file", "desktop-mode"), codeNode(row.plugin));
7741 if (row.version) {
7742 pushFactCard(grid, "tag", __("Version", "desktop-mode"), row.version);
7743 }
7744 if (row.desktop_mode_size_kb !== null && row.desktop_mode_size_kb !== void 0) {
7745 pushFactCard(
7746 grid,
7747 "database",
7748 __("Size on disk", "desktop-mode"),
7749 formatSize$1(row.desktop_mode_size_kb)
7750 );
7751 }
7752 if (row.requires_wp) {
7753 pushFactCard(
7754 grid,
7755 "wordpress-alt",
7756 __("Requires WordPress", "desktop-mode"),
7757 sprintf(
7758 /* translators: %s: version */
7759 __("%s+", "desktop-mode"),
7760 row.requires_wp
7761 )
7762 );
7763 }
7764 if (row.requires_php) {
7765 pushFactCard(
7766 grid,
7767 "editor-code",
7768 __("Requires PHP", "desktop-mode"),
7769 sprintf(
7770 /* translators: %s: version */
7771 __("%s+", "desktop-mode"),
7772 row.requires_php
7773 )
7774 );
7775 }
7776 if (row.textdomain) {
7777 pushFactCard(
7778 grid,
7779 "translation",
7780 __("Text domain", "desktop-mode"),
7781 codeNode(String(row.textdomain))
7782 );
7783 }
7784 if (row.plugin_uri) {
7785 pushFactCard(grid, "admin-links", __("Plugin URL", "desktop-mode"), externalLink(row.plugin_uri));
7786 }
7787 if (row.author_uri) {
7788 pushFactCard(grid, "admin-users", __("Author URL", "desktop-mode"), externalLink(row.author_uri));
7789 }
7790 if (row.network_only) {
7791 pushFactCard(
7792 grid,
7793 "networking",
7794 __("Scope", "desktop-mode"),
7795 __("Network only", "desktop-mode")
7796 );
7797 }
7798 pushFactCard(
7799 grid,
7800 row.status === "active" || row.status === "active-network" ? "yes-alt" : "marker",
7801 __("Status", "desktop-mode"),
7802 row.status === "active" || row.status === "active-network" ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode")
7803 );
7804 return grid;
7805 }
7806 function pushFactCard(parent, icon, label, value) {
7807 const card = document.createElement("wpd-card");
7808 card.setAttribute("compact", "");
7809 card.className = "desktop-mode-plugins__detail-fact";
7810 const head = document.createElement("div");
7811 head.setAttribute("slot", "header");
7812 head.className = "desktop-mode-plugins__detail-fact-head";
7813 const ico = document.createElement("span");
7814 ico.className = `dashicons dashicons-${icon}`;
7815 ico.setAttribute("aria-hidden", "true");
7816 const lab = document.createElement("span");
7817 lab.className = "desktop-mode-plugins__detail-fact-label";
7818 lab.textContent = label;
7819 head.append(ico, lab);
7820 card.appendChild(head);
7821 const val = document.createElement("div");
7822 val.className = "desktop-mode-plugins__detail-fact-value";
7823 if (typeof value === "string") {
7824 val.textContent = value;
7825 } else {
7826 val.appendChild(value);
7827 }
7828 card.appendChild(val);
7829 parent.appendChild(card);
7830 }
7831 function renderChangelog(info) {
7832 if (!info) {
7833 return buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode"));
7834 }
7835 const html2 = info.sections?.changelog;
7836 if (!html2) {
7837 return buildEmpty(
7838 "list-view",
7839 __("No changelog", "desktop-mode"),
7840 __("This plugin doesn’t ship a changelog.", "desktop-mode")
7841 );
7842 }
7843 const entries = parseChangelogEntries(html2);
7844 if (entries.length === 0) {
7845 const wrap = document.createElement("div");
7846 wrap.className = "desktop-mode-plugins__detail-html";
7847 wrap.innerHTML = sanitizeHtml(html2);
7848 sanitizeLinks(wrap);
7849 return wrap;
7850 }
7851 const stack = document.createElement("wpd-stack");
7852 stack.setAttribute("gap", "12");
7853 stack.className = "desktop-mode-plugins__detail-changelog";
7854 entries.forEach((entry, i) => {
7855 const card = document.createElement("wpd-card");
7856 card.className = "desktop-mode-plugins__detail-changelog-entry";
7857 const head = document.createElement("div");
7858 head.setAttribute("slot", "header");
7859 head.className = "desktop-mode-plugins__detail-changelog-head";
7860 const ver = document.createElement("wpd-badge");
7861 ver.setAttribute("tone", i === 0 ? "success" : "neutral");
7862 ver.textContent = entry.version;
7863 head.appendChild(ver);
7864 if (i === 0) {
7865 const latest = document.createElement("span");
7866 latest.className = "desktop-mode-plugins__detail-changelog-latest";
7867 latest.textContent = __("Latest", "desktop-mode");
7868 head.appendChild(latest);
7869 }
7870 card.appendChild(head);
7871 const body = document.createElement("div");
7872 body.className = "desktop-mode-plugins__detail-html";
7873 body.innerHTML = sanitizeHtml(entry.body);
7874 sanitizeLinks(body);
7875 card.appendChild(body);
7876 stack.appendChild(card);
7877 });
7878 return stack;
7879 }
7880 function parseChangelogEntries(html2) {
7881 const tmp = document.createElement("div");
7882 tmp.innerHTML = html2;
7883 if (tmp.childNodes.length === 0) {
7884 return [];
7885 }
7886 const entries = [];
7887 let current = null;
7888 const versionRegex = /([0-9]+\.[0-9]+(?:\.[0-9]+)?(?:[\w.+-]*)?)/;
7889 const flush = () => {
7890 if (!current) {
7891 return;
7892 }
7893 entries.push({ version: current.version, body: current.html.trim() });
7894 current = null;
7895 };
7896 for (const node of Array.from(tmp.childNodes)) {
7897 if (node.nodeType === Node.ELEMENT_NODE) {
7898 const el = node;
7899 const isHeading = /^H[1-6]$/.test(el.tagName);
7900 const text = (el.textContent ?? "").trim();
7901 const cleaned = text.replace(/^=+\s*|\s*=+$/g, "").trim();
7902 const headingMatch = isHeading ? cleaned.match(versionRegex) : null;
7903 if (headingMatch) {
7904 flush();
7905 current = { version: cleaned, html: "" };
7906 continue;
7907 }
7908 if (!current) {
7909 continue;
7910 }
7911 current.html += el.outerHTML;
7912 continue;
7913 }
7914 if (node.nodeType === Node.TEXT_NODE) {
7915 const text = node.textContent ?? "";
7916 if (!current) {
7917 continue;
7918 }
7919 if (text.trim() === "") {
7920 if (current.html !== "") {
7921 current.html += text;
7922 }
7923 continue;
7924 }
7925 current.html += `<p>${escapeHtml$1(text)}</p>`;
7926 }
7927 }
7928 flush();
7929 return entries;
7930 }
7931 function renderFaq(info) {
7932 if (!info) {
7933 return buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode"));
7934 }
7935 const html2 = info.sections?.faq;
7936 if (!html2) {
7937 return buildEmpty(
7938 "editor-help",
7939 __("No FAQ", "desktop-mode"),
7940 __("This plugin doesn’t ship an FAQ.", "desktop-mode")
7941 );
7942 }
7943 const pairs = parseFaqPairs(html2);
7944 if (pairs.length === 0) {
7945 const wrap = document.createElement("div");
7946 wrap.className = "desktop-mode-plugins__detail-html";
7947 wrap.innerHTML = sanitizeHtml(html2);
7948 sanitizeLinks(wrap);
7949 return wrap;
7950 }
7951 const stack = document.createElement("wpd-stack");
7952 stack.setAttribute("gap", "8");
7953 stack.className = "desktop-mode-plugins__detail-faq";
7954 pairs.forEach((pair, i) => {
7955 const item = document.createElement("details");
7956 item.className = "desktop-mode-plugins__detail-faq-item";
7957 if (i === 0) {
7958 item.setAttribute("open", "");
7959 }
7960 const summary = document.createElement("summary");
7961 summary.className = "desktop-mode-plugins__detail-faq-q";
7962 const qText = document.createElement("span");
7963 qText.className = "desktop-mode-plugins__detail-faq-q-text";
7964 qText.textContent = pair.question;
7965 const chevron = document.createElement("span");
7966 chevron.className = "desktop-mode-plugins__detail-faq-chevron";
7967 chevron.setAttribute("aria-hidden", "true");
7968 chevron.innerHTML = '<svg viewBox="0 0 12 12" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5 L6 7.5 L9 4.5"/></svg>';
7969 summary.append(qText, chevron);
7970 const body = document.createElement("div");
7971 body.className = "desktop-mode-plugins__detail-faq-a desktop-mode-plugins__detail-html";
7972 body.innerHTML = sanitizeHtml(pair.answer);
7973 sanitizeLinks(body);
7974 item.append(summary, body);
7975 stack.appendChild(item);
7976 });
7977 return stack;
7978 }
7979 function parseFaqPairs(html2) {
7980 const tmp = document.createElement("div");
7981 tmp.innerHTML = html2;
7982 const dts = Array.from(tmp.querySelectorAll(":scope > dt"));
7983 if (dts.length > 0) {
7984 const pairs2 = [];
7985 for (const dt of dts) {
7986 pairs2.push(splitDtIntoPair(dt));
7987 }
7988 return pairs2.filter((p) => p.question !== "");
7989 }
7990 const dl = tmp.querySelector(":scope > dl");
7991 if (dl) {
7992 const pairs2 = [];
7993 let current2 = null;
7994 for (const node of Array.from(dl.children)) {
7995 if (node.tagName === "DT") {
7996 if (current2) {
7997 pairs2.push({ question: current2.q, answer: current2.html.trim() });
7998 }
7999 current2 = { q: (node.textContent ?? "").trim(), html: "" };
8000 } else if (node.tagName === "DD" && current2) {
8001 current2.html += node.innerHTML;
8002 } else if (current2) {
8003 current2.html += node.outerHTML;
8004 }
8005 }
8006 if (current2) {
8007 pairs2.push({ question: current2.q, answer: current2.html.trim() });
8008 }
8009 return pairs2.filter((p) => p.question !== "");
8010 }
8011 const pairs = [];
8012 let current = null;
8013 const flush = () => {
8014 if (!current) {
8015 return;
8016 }
8017 pairs.push({ question: current.q, answer: current.html.trim() });
8018 current = null;
8019 };
8020 for (const node of Array.from(tmp.childNodes)) {
8021 if (node.nodeType === Node.ELEMENT_NODE) {
8022 const el = node;
8023 const isHeading = /^H[1-6]$/.test(el.tagName);
8024 const text = (el.textContent ?? "").trim();
8025 if (isHeading && text) {
8026 flush();
8027 current = { q: text, html: "" };
8028 continue;
8029 }
8030 if (!current) {
8031 continue;
8032 }
8033 current.html += el.outerHTML;
8034 continue;
8035 }
8036 if (node.nodeType === Node.TEXT_NODE && current) {
8037 const text = node.textContent ?? "";
8038 if (text.trim() === "") {
8039 if (current.html !== "") {
8040 current.html += text;
8041 }
8042 continue;
8043 }
8044 current.html += `<p>${escapeHtml$1(text)}</p>`;
8045 }
8046 }
8047 flush();
8048 return pairs.filter((p) => p.question !== "");
8049 }
8050 function splitDtIntoPair(dt) {
8051 let question = "";
8052 let answerHtml = "";
8053 let seenElement = false;
8054 for (const child of Array.from(dt.childNodes)) {
8055 if (child.nodeType === Node.TEXT_NODE) {
8056 if (!seenElement) {
8057 question += child.textContent ?? "";
8058 } else {
8059 const txt = child.textContent ?? "";
8060 if (txt.trim() !== "") {
8061 answerHtml += `<p>${escapeHtml$1(txt)}</p>`;
8062 }
8063 }
8064 continue;
8065 }
8066 if (child.nodeType !== Node.ELEMENT_NODE) {
8067 continue;
8068 }
8069 const el = child;
8070 if (el.tagName === "P" && (el.textContent ?? "").trim() === "") {
8071 continue;
8072 }
8073 seenElement = true;
8074 answerHtml += el.outerHTML;
8075 }
8076 return {
8077 question: question.replace(/\s+/g, " ").trim(),
8078 answer: answerHtml.trim()
8079 };
8080 }
8081 function escapeHtml$1(text) {
8082 const tmp = document.createElement("span");
8083 tmp.textContent = text;
8084 return tmp.innerHTML;
8085 }
8086 function renderReviews(slug, info) {
8087 const stack = document.createElement("wpd-stack");
8088 stack.setAttribute("gap", "16");
8089 if (!info) {
8090 stack.appendChild(buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode")));
8091 return stack;
8092 }
8093 stack.appendChild(buildHistogram(info));
8094 const body = document.createElement("div");
8095 body.className = "desktop-mode-plugins__detail-reviews";
8096 stack.appendChild(body);
8097 const cached = reviewsCache.get(slug);
8098 if (cached) {
8099 paintReviewList(body, cached, slug);
8100 } else {
8101 body.appendChild(buildLoadingBlock(__("Loading recent reviews…", "desktop-mode")));
8102 void (async () => {
8103 try {
8104 const resp = await fetchPluginReviews(slug);
8105 reviewsCache.set(slug, resp);
8106 if (body.isConnected) {
8107 paintReviewList(body, resp, slug);
8108 }
8109 } catch {
8110 if (body.isConnected) {
8111 body.replaceChildren(
8112 buildEmpty(
8113 "warning",
8114 __("Couldn’t load reviews", "desktop-mode"),
8115 __("WordPress.org didn’t respond. Try again in a moment.", "desktop-mode")
8116 )
8117 );
8118 }
8119 }
8120 })();
8121 }
8122 return stack;
8123 }
8124 function paintReviewList(host, resp, slug) {
8125 host.replaceChildren();
8126 if (!resp.parsed) {
8127 host.appendChild(buildReviewsFallback(slug));
8128 return;
8129 }
8130 if (resp.items.length === 0) {
8131 host.appendChild(buildWriteReviewCta(slug));
8132 return;
8133 }
8134 const grid = document.createElement("wpd-grid");
8135 grid.setAttribute("columns", "2");
8136 grid.setAttribute("gap", "12");
8137 grid.className = "desktop-mode-plugins__detail-reviews-grid";
8138 for (const item of resp.items) {
8139 grid.appendChild(buildReviewCard(item));
8140 }
8141 host.appendChild(grid);
8142 const more = document.createElement("div");
8143 more.className = "desktop-mode-plugins__detail-reviews-more";
8144 more.appendChild(
8145 linkButton(
8146 "ghost",
8147 __("Read all reviews on WordPress.org ↗", "desktop-mode"),
8148 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/#reviews`
8149 )
8150 );
8151 host.appendChild(more);
8152 }
8153 function buildReviewsFallback(slug) {
8154 const empty = buildEmpty(
8155 "external",
8156 __("Reviews live on WordPress.org", "desktop-mode"),
8157 __(
8158 "We couldn’t pull the review feed here. Open the full thread on WordPress.org to read every review.",
8159 "desktop-mode"
8160 )
8161 );
8162 const cta = document.createElement("wpd-button");
8163 cta.setAttribute("slot", "cta");
8164 cta.setAttribute("variant", "primary");
8165 cta.setAttribute("size", "small");
8166 cta.setAttribute("data-noclick", "");
8167 cta.textContent = __("Open reviews on WordPress.org ↗", "desktop-mode");
8168 cta.addEventListener("click", () => {
8169 window.open(
8170 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/#reviews`,
8171 "_blank",
8172 "noopener,noreferrer"
8173 );
8174 });
8175 empty.appendChild(cta);
8176 return empty;
8177 }
8178 function buildWriteReviewCta(slug) {
8179 const wrap = document.createElement("div");
8180 wrap.className = "desktop-mode-plugins__detail-reviews-cta";
8181 wrap.appendChild(
8182 linkButton(
8183 "primary",
8184 __("Write a review on WordPress.org ↗", "desktop-mode"),
8185 `https://wordpress.org/support/plugin/${encodeURIComponent(slug)}/reviews/#new-post`
8186 )
8187 );
8188 return wrap;
8189 }
8190 function buildReviewCard(item) {
8191 const card = document.createElement("wpd-card");
8192 card.setAttribute("compact", "");
8193 card.className = "desktop-mode-plugins__detail-review";
8194 const head = document.createElement("div");
8195 head.setAttribute("slot", "header");
8196 head.className = "desktop-mode-plugins__detail-review-head";
8197 const author = document.createElement("strong");
8198 author.textContent = item.author || __("Anonymous", "desktop-mode");
8199 head.appendChild(author);
8200 const stars = buildStarCluster(item.stars / 5 * 100, 0);
8201 head.appendChild(stars);
8202 if (item.date) {
8203 const date = document.createElement("span");
8204 date.className = "desktop-mode-plugins__detail-review-date";
8205 date.textContent = item.date;
8206 head.appendChild(date);
8207 }
8208 card.appendChild(head);
8209 if (item.excerpt) {
8210 const body = document.createElement("p");
8211 body.className = "desktop-mode-plugins__detail-review-body";
8212 body.textContent = item.excerpt;
8213 card.appendChild(body);
8214 }
8215 if (item.url) {
8216 const foot = document.createElement("div");
8217 foot.setAttribute("slot", "footer");
8218 const link = document.createElement("a");
8219 link.href = item.url;
8220 link.target = "_blank";
8221 link.rel = "noopener noreferrer";
8222 link.setAttribute("data-noclick", "");
8223 link.textContent = __("Read full review ↗", "desktop-mode");
8224 link.className = "desktop-mode-plugins__detail-review-link";
8225 foot.appendChild(link);
8226 card.appendChild(foot);
8227 }
8228 return card;
8229 }
8230 function buildHistogram(info) {
8231 const el = document.createElement("wpd-rating-summary");
8232 if (typeof info.rating === "number") {
8233 el.setAttribute("rating", String(info.rating));
8234 }
8235 if (info.num_ratings) {
8236 el.setAttribute("total", String(info.num_ratings));
8237 }
8238 const buckets = {};
8239 const ratings = info.ratings ?? {};
8240 for (const key of ["1", "2", "3", "4", "5"]) {
8241 const v = ratings[key];
8242 if (typeof v === "number") {
8243 buckets[key] = v;
8244 }
8245 }
8246 el.ratings = buckets;
8247 return el;
8248 }
8249 function chip(icon, label) {
8250 const c = document.createElement("wpd-chip");
8251 c.setAttribute("label", label);
8252 c.setAttribute("tone", "neutral");
8253 const ico = document.createElement("span");
8254 ico.setAttribute("slot", "icon");
8255 ico.className = `dashicons dashicons-${icon}`;
8256 ico.setAttribute("aria-hidden", "true");
8257 c.appendChild(ico);
8258 return c;
8259 }
8260 function buildLoadingBlock(label) {
8261 const wrap = document.createElement("div");
8262 wrap.className = "desktop-mode-plugins__detail-loading-block";
8263 const spinner = document.createElement("wpd-spinner");
8264 spinner.setAttribute("preset", "classic");
8265 spinner.setAttribute("size", "20");
8266 wrap.appendChild(spinner);
8267 const text = document.createElement("span");
8268 text.textContent = label;
8269 wrap.appendChild(text);
8270 return wrap;
8271 }
8272 function buildEmpty(icon, heading, description) {
8273 const e = document.createElement("wpd-empty-state");
8274 e.setAttribute("icon", `dashicons-${icon}`);
8275 e.setAttribute("heading", heading);
8276 e.setAttribute("description", description);
8277 return e;
8278 }
8279 function buildFallbackGlyph() {
8280 const span = document.createElement("span");
8281 span.className = "dashicons dashicons-admin-plugins";
8282 span.setAttribute("aria-hidden", "true");
8283 return span;
8284 }
8285 function linkButton(variant, label, href) {
8286 const btn = document.createElement("wpd-button");
8287 btn.setAttribute("variant", variant);
8288 btn.setAttribute("size", "small");
8289 btn.textContent = label;
8290 btn.setAttribute("data-noclick", "");
8291 btn.addEventListener("click", () => {
8292 window.open(href, "_blank", "noopener,noreferrer");
8293 });
8294 return btn;
8295 }
8296 function codeNode(text) {
8297 const code = document.createElement("code");
8298 code.textContent = text;
8299 return code;
8300 }
8301 function externalLink(href) {
8302 const a = document.createElement("a");
8303 a.href = href;
8304 a.target = "_blank";
8305 a.rel = "noopener noreferrer";
8306 a.textContent = href;
8307 a.setAttribute("data-noclick", "");
8308 return a;
8309 }
8310 function sanitizeLinks(wrap) {
8311 wrap.querySelectorAll("a").forEach((a) => {
8312 a.setAttribute("target", "_blank");
8313 a.setAttribute("rel", "noopener noreferrer");
8314 a.setAttribute("data-noclick", "");
8315 });
8316 }
8317 function deriveSlug(row) {
8318 if (!row.desktop_mode_icon_url) {
8319 return "";
8320 }
8321 const fromUpdate = row.desktop_mode_update_available?.slug;
8322 if (fromUpdate) {
8323 return fromUpdate;
8324 }
8325 const file = typeof row.plugin === "string" ? row.plugin : "";
8326 if (file) {
8327 const slash = file.indexOf("/");
8328 if (slash > 0) {
8329 return file.slice(0, slash);
8330 }
8331 }
8332 if (row.textdomain) {
8333 return String(row.textdomain);
8334 }
8335 return "";
8336 }
8337 function readDescription(row) {
8338 const d = row.description;
8339 if (!d) {
8340 return "";
8341 }
8342 if (typeof d === "string") {
8343 return d;
8344 }
8345 return d.rendered || d.raw || "";
8346 }
8347 function formatSize$1(kb) {
8348 if (kb < 1024) {
8349 return sprintf(
8350 /* translators: %d: kilobytes */
8351 __("%d KB", "desktop-mode"),
8352 kb
8353 );
8354 }
8355 return sprintf(
8356 /* translators: %s: megabytes (one decimal) */
8357 __("%s MB", "desktop-mode"),
8358 (kb / 1024).toFixed(1)
8359 );
8360 }
8361 function humanDate(raw) {
8362 const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raw);
8363 if (!m) {
8364 return raw;
8365 }
8366 try {
8367 return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])).toLocaleDateString();
8368 } catch {
8369 return raw;
8370 }
8371 }
8372 function stripHtml$1(html2) {
8373 const tmp = document.createElement("div");
8374 tmp.innerHTML = html2;
8375 return tmp.textContent ?? "";
8376 }
8377 function sanitizeHtml(html2) {
8378 const allowed = /* @__PURE__ */ new Set([
8379 "A",
8380 "ABBR",
8381 "B",
8382 "BLOCKQUOTE",
8383 "BR",
8384 "CODE",
8385 "DD",
8386 "DEL",
8387 "DIV",
8388 "DL",
8389 "DT",
8390 "EM",
8391 "FIGCAPTION",
8392 "FIGURE",
8393 "H1",
8394 "H2",
8395 "H3",
8396 "H4",
8397 "H5",
8398 "H6",
8399 "HR",
8400 "I",
8401 "IMG",
8402 "KBD",
8403 "LI",
8404 "OL",
8405 "P",
8406 "PRE",
8407 "Q",
8408 "S",
8409 "SMALL",
8410 "SPAN",
8411 "STRONG",
8412 "SUB",
8413 "SUP",
8414 "TABLE",
8415 "TBODY",
8416 "TD",
8417 "TFOOT",
8418 "TH",
8419 "THEAD",
8420 "TR",
8421 "U",
8422 "UL"
8423 ]);
8424 const allowedAttrs = /* @__PURE__ */ new Set([
8425 "href",
8426 "src",
8427 "alt",
8428 "title",
8429 "name",
8430 "rel",
8431 "target",
8432 "colspan",
8433 "rowspan"
8434 ]);
8435 const wrap = document.createElement("div");
8436 wrap.innerHTML = html2;
8437 const walker = document.createTreeWalker(wrap, NodeFilter.SHOW_ELEMENT);
8438 const toRemove = [];
8439 let current = walker.currentNode;
8440 while (current) {
8441 const next = walker.nextNode();
8442 if (current === wrap) {
8443 current = next;
8444 continue;
8445 }
8446 if (!allowed.has(current.tagName)) {
8447 toRemove.push(current);
8448 } else {
8449 for (const attr of Array.from(current.attributes)) {
8450 if (!allowedAttrs.has(attr.name.toLowerCase())) {
8451 current.removeAttribute(attr.name);
8452 }
8453 }
8454 if (current.tagName === "A") {
8455 const href = current.getAttribute("href") ?? "";
8456 if (href.toLowerCase().startsWith("javascript:")) {
8457 current.removeAttribute("href");
8458 }
8459 }
8460 if (current.tagName === "IMG") {
8461 const src = current.getAttribute("src") ?? "";
8462 if (src.toLowerCase().startsWith("javascript:")) {
8463 current.removeAttribute("src");
8464 }
8465 }
8466 }
8467 current = next;
8468 }
8469 for (const el of toRemove) {
8470 const text = document.createTextNode(el.textContent ?? "");
8471 el.replaceWith(text);
8472 }
8473 return wrap.innerHTML;
8474 }
8475 const PANEL_STYLES = `
8476 .desktop-mode-plugins__detail {
8477 display: block;
8478 background: var( --wpd-surface-subtle, rgba( 0, 0, 0, 0.025 ) );
8479 border-block-start: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8480 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8481 color: var( --wpd-fg, inherit );
8482 font-size: 13px;
8483 line-height: 1.55;
8484 }
8485
8486 /* Hero */
8487 .desktop-mode-plugins__detail-hero {
8488 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.6 ) );
8489 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8490 }
8491 .desktop-mode-plugins__detail-hero-inner {
8492 display: flex;
8493 align-items: center;
8494 gap: 14px;
8495 padding: 14px 24px;
8496 }
8497 .desktop-mode-plugins__detail-hero-icon {
8498 flex: 0 0 44px;
8499 width: 44px;
8500 height: 44px;
8501 border-radius: 10px;
8502 overflow: hidden;
8503 background: var( --wpd-surface, rgba( 0, 0, 0, 0.04 ) );
8504 box-shadow: 0 0 0 1px var( --wpd-border, rgba( 0, 0, 0, 0.08 ) ) inset;
8505 display: flex;
8506 align-items: center;
8507 justify-content: center;
8508 }
8509 .desktop-mode-plugins__detail-hero-icon img {
8510 width: 100%;
8511 height: 100%;
8512 max-width: 100%;
8513 max-height: 100%;
8514 object-fit: contain;
8515 display: block;
8516 }
8517 .desktop-mode-plugins__detail-hero-icon .dashicons {
8518 font-size: 20px;
8519 width: 20px;
8520 height: 20px;
8521 line-height: 20px;
8522 color: var( --wpd-fg-muted, #888 );
8523 }
8524 .desktop-mode-plugins__detail-hero-text {
8525 flex: 1 1 auto;
8526 min-width: 0;
8527 }
8528 .desktop-mode-plugins__detail-title {
8529 margin: 0;
8530 font-size: 15px;
8531 font-weight: 600;
8532 line-height: 1.25;
8533 letter-spacing: -0.005em;
8534 color: var( --wpd-fg, inherit );
8535 }
8536 .desktop-mode-plugins__detail-byline {
8537 margin: 0;
8538 font-size: 12.5px;
8539 color: var( --wpd-fg-muted, #666 );
8540 }
8541 .desktop-mode-plugins__detail-byline a {
8542 color: inherit;
8543 text-decoration: underline;
8544 text-decoration-color: var( --wpd-border-strong, rgba( 0, 0, 0, 0.25 ) );
8545 }
8546 .desktop-mode-plugins__detail-byline a:hover {
8547 color: var( --wp-admin-theme-color, #2271b1 );
8548 }
8549
8550 /* Tab strip */
8551 .desktop-mode-plugins__detail-tabs-wrap {
8552 padding: 0 24px;
8553 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.6 ) );
8554 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8555 }
8556 .desktop-mode-plugins__detail-tabs {
8557 display: block;
8558 }
8559
8560 /* Body */
8561 .desktop-mode-plugins__detail-body {
8562 padding: 22px 24px 26px;
8563 max-width: 100%;
8564 }
8565
8566 /* Overview chip strip */
8567 .desktop-mode-plugins__detail-chip-strip {
8568 display: flex;
8569 flex-wrap: wrap;
8570 gap: 8px;
8571 align-items: center;
8572 }
8573 .desktop-mode-plugins__detail-stars-pill {
8574 display: inline-flex;
8575 align-items: center;
8576 gap: 6px;
8577 padding: 4px 12px;
8578 border-radius: 999px;
8579 background: rgba( 234, 179, 8, 0.12 );
8580 color: #8a5a00;
8581 font-size: 12px;
8582 font-weight: 600;
8583 }
8584 .desktop-mode-plugins__detail-actions {
8585 padding-top: 4px;
8586 }
8587
8588 /* Sanitized HTML body (description / changelog / FAQ answers) */
8589 .desktop-mode-plugins__detail-html {
8590 color: var( --wpd-fg, inherit );
8591 font-size: 14px;
8592 line-height: 1.65;
8593 max-width: 78ch;
8594 }
8595 .desktop-mode-plugins__detail-html h1,
8596 .desktop-mode-plugins__detail-html h2,
8597 .desktop-mode-plugins__detail-html h3,
8598 .desktop-mode-plugins__detail-html h4 {
8599 margin: 16px 0 6px;
8600 line-height: 1.3;
8601 font-weight: 600;
8602 }
8603 .desktop-mode-plugins__detail-html h1 { font-size: 18px; }
8604 .desktop-mode-plugins__detail-html h2 { font-size: 16px; }
8605 .desktop-mode-plugins__detail-html h3 { font-size: 14.5px; }
8606 .desktop-mode-plugins__detail-html h4 { font-size: 13.5px; }
8607 .desktop-mode-plugins__detail-html p {
8608 margin: 0 0 10px;
8609 }
8610 .desktop-mode-plugins__detail-html ul,
8611 .desktop-mode-plugins__detail-html ol {
8612 margin: 0 0 10px;
8613 padding-inline-start: 22px;
8614 }
8615 .desktop-mode-plugins__detail-html li { margin-bottom: 4px; }
8616 .desktop-mode-plugins__detail-html code,
8617 .desktop-mode-plugins__detail-html pre {
8618 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8619 font-size: 12px;
8620 background: rgba( 0, 0, 0, 0.06 );
8621 border-radius: 4px;
8622 }
8623 .desktop-mode-plugins__detail-html code { padding: 1px 6px; }
8624 .desktop-mode-plugins__detail-html pre {
8625 padding: 10px 12px;
8626 overflow-x: auto;
8627 margin: 0 0 10px;
8628 }
8629 .desktop-mode-plugins__detail-html pre code {
8630 background: transparent;
8631 padding: 0;
8632 }
8633 .desktop-mode-plugins__detail-html a {
8634 color: var( --wp-admin-theme-color, #2271b1 );
8635 }
8636 .desktop-mode-plugins__detail-html img {
8637 display: block;
8638 max-width: 100%;
8639 max-height: 220px;
8640 width: auto;
8641 height: auto;
8642 object-fit: contain;
8643 margin: 8px 0;
8644 border-radius: 6px;
8645 }
8646
8647 /* Details fact cards */
8648 .desktop-mode-plugins__detail-grid {
8649 width: 100%;
8650 }
8651 .desktop-mode-plugins__detail-fact {
8652 min-width: 0;
8653 }
8654 .desktop-mode-plugins__detail-fact-head {
8655 display: flex;
8656 align-items: center;
8657 gap: 8px;
8658 color: var( --wpd-fg-muted, #666 );
8659 font-size: 11px;
8660 font-weight: 600;
8661 letter-spacing: 0.06em;
8662 text-transform: uppercase;
8663 }
8664 .desktop-mode-plugins__detail-fact-head .dashicons {
8665 font-size: 14px;
8666 width: 14px;
8667 height: 14px;
8668 line-height: 14px;
8669 }
8670 .desktop-mode-plugins__detail-fact-value {
8671 font-size: 14px;
8672 color: var( --wpd-fg, inherit );
8673 word-break: break-word;
8674 overflow-wrap: anywhere;
8675 font-weight: 500;
8676 }
8677 .desktop-mode-plugins__detail-fact-value code {
8678 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8679 font-size: 12.5px;
8680 background: rgba( 0, 0, 0, 0.06 );
8681 padding: 2px 7px;
8682 border-radius: 4px;
8683 font-weight: 400;
8684 }
8685 .desktop-mode-plugins__detail-fact-value a {
8686 color: var( --wp-admin-theme-color, #2271b1 );
8687 text-decoration: none;
8688 }
8689 .desktop-mode-plugins__detail-fact-value a:hover {
8690 text-decoration: underline;
8691 }
8692
8693 /* Changelog — version-grouped cards */
8694 .desktop-mode-plugins__detail-changelog {
8695 width: 100%;
8696 }
8697 .desktop-mode-plugins__detail-changelog-entry {
8698 width: 100%;
8699 }
8700 .desktop-mode-plugins__detail-changelog-head {
8701 display: flex;
8702 align-items: center;
8703 gap: 10px;
8704 }
8705 .desktop-mode-plugins__detail-changelog-latest {
8706 font-size: 11px;
8707 font-weight: 600;
8708 letter-spacing: 0.06em;
8709 text-transform: uppercase;
8710 color: var( --wpd-fg-muted, #666 );
8711 }
8712
8713 /* FAQ — accordion */
8714 .desktop-mode-plugins__detail-faq {
8715 width: 100%;
8716 }
8717 .desktop-mode-plugins__detail-faq-item {
8718 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.7 ) );
8719 border: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8720 border-radius: 12px;
8721 overflow: hidden;
8722 transition: box-shadow 160ms ease, border-color 160ms ease;
8723 }
8724 .desktop-mode-plugins__detail-faq-item[open] {
8725 border-color: var( --wp-admin-theme-color, #2271b1 );
8726 box-shadow: 0 4px 14px rgba( 0, 0, 0, 0.06 );
8727 }
8728 .desktop-mode-plugins__detail-faq-q {
8729 display: flex;
8730 align-items: center;
8731 gap: 10px;
8732 padding: 14px 16px;
8733 cursor: pointer;
8734 list-style: none;
8735 user-select: none;
8736 }
8737 .desktop-mode-plugins__detail-faq-q::-webkit-details-marker {
8738 display: none;
8739 }
8740 .desktop-mode-plugins__detail-faq-q:hover {
8741 background: rgba( 0, 0, 0, 0.025 );
8742 }
8743 .desktop-mode-plugins__detail-faq-q-text {
8744 flex: 1 1 auto;
8745 font-size: 14px;
8746 font-weight: 600;
8747 color: var( --wpd-fg, inherit );
8748 line-height: 1.4;
8749 }
8750 .desktop-mode-plugins__detail-faq-chevron {
8751 flex: 0 0 auto;
8752 width: 24px;
8753 height: 24px;
8754 border-radius: 50%;
8755 display: inline-flex;
8756 align-items: center;
8757 justify-content: center;
8758 background: rgba( 0, 0, 0, 0.05 );
8759 color: var( --wpd-fg-muted, #555 );
8760 transition: transform 200ms cubic-bezier( 0.2, 0.8, 0.2, 1 ), background 160ms ease;
8761 }
8762 .desktop-mode-plugins__detail-faq-item[open] .desktop-mode-plugins__detail-faq-chevron {
8763 transform: rotate( 180deg );
8764 background: var( --wp-admin-theme-color, #2271b1 );
8765 color: #fff;
8766 }
8767 .desktop-mode-plugins__detail-faq-a {
8768 padding: 4px 16px 16px;
8769 border-block-start: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.06 ) );
8770 background: rgba( 0, 0, 0, 0.012 );
8771 }
8772 @media ( prefers-reduced-motion: reduce ) {
8773 .desktop-mode-plugins__detail-faq-chevron,
8774 .desktop-mode-plugins__detail-faq-item {
8775 transition: none;
8776 }
8777 }
8778
8779 /* Reviews */
8780 .desktop-mode-plugins__detail-reviews {
8781 width: 100%;
8782 }
8783 .desktop-mode-plugins__detail-reviews-grid {
8784 width: 100%;
8785 }
8786 .desktop-mode-plugins__detail-reviews-more,
8787 .desktop-mode-plugins__detail-reviews-cta {
8788 display: flex;
8789 justify-content: center;
8790 padding-top: 12px;
8791 }
8792 .desktop-mode-plugins__detail-review {
8793 width: 100%;
8794 height: 100%;
8795 box-sizing: border-box;
8796 }
8797 .desktop-mode-plugins__detail-review-body {
8798 display: -webkit-box;
8799 -webkit-line-clamp: 4;
8800 -webkit-box-orient: vertical;
8801 overflow: hidden;
8802 }
8803 @media ( max-width: 720px ) {
8804 .desktop-mode-plugins__detail-reviews-grid {
8805 grid-template-columns: 1fr !important;
8806 }
8807 }
8808 .desktop-mode-plugins__detail-review-head {
8809 display: flex;
8810 align-items: center;
8811 gap: 10px;
8812 flex-wrap: wrap;
8813 }
8814 .desktop-mode-plugins__detail-review-date {
8815 margin-inline-start: auto;
8816 font-size: 11.5px;
8817 color: var( --wpd-fg-muted, #888 );
8818 }
8819 .desktop-mode-plugins__detail-review-body {
8820 margin: 0;
8821 font-size: 13px;
8822 color: var( --wpd-fg, inherit );
8823 line-height: 1.55;
8824 }
8825 .desktop-mode-plugins__detail-review-link {
8826 font-size: 12px;
8827 font-weight: 600;
8828 color: var( --wp-admin-theme-color, #2271b1 );
8829 text-decoration: none;
8830 }
8831 .desktop-mode-plugins__detail-review-link:hover {
8832 text-decoration: underline;
8833 }
8834
8835 /* Loading block */
8836 .desktop-mode-plugins__detail-loading-block {
8837 display: inline-flex;
8838 align-items: center;
8839 gap: 10px;
8840 padding: 12px 14px;
8841 border-radius: 10px;
8842 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.7 ) );
8843 border: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8844 color: var( --wpd-fg-muted, #666 );
8845 font-size: 13px;
8846 }
8847
8848 @media ( max-width: 720px ) {
8849 .desktop-mode-plugins__detail-grid {
8850 grid-template-columns: 1fr !important;
8851 }
8852 }
8853 `;
8854 const PLUGINS_CHANGED_TOPIC = "desktop-mode.plugin.changed";
8855 const SOURCE = "installed-view";
8856 function toast(message, duration = 3500) {
8857 const api2 = window.wp?.desktop;
8858 if (api2 && typeof api2.showToast === "function") {
8859 api2.showToast({ message, duration });
8860 return;
8861 }
8862 console.log("[plugins-window]", message);
8863 }
8864 async function confirm(opts) {
8865 const api2 = window.wp?.desktop;
8866 if (api2 && typeof api2.confirm === "function") {
8867 return api2.confirm(opts);
8868 }
8869 return Promise.resolve(true);
8870 }
8871 function mountInstalledView(host) {
8872 host.replaceChildren();
8873 const state = {
8874 rows: [],
8875 statusFilter: "",
8876 search: "",
8877 loading: true,
8878 updating: /* @__PURE__ */ new Set(),
8879 autoUpdating: /* @__PURE__ */ new Set()
8880 };
8881 const toolbar = document.createElement("header");
8882 toolbar.className = "desktop-mode-plugins__toolbar";
8883 const left = document.createElement("div");
8884 left.className = "desktop-mode-plugins__toolbar-left";
8885 const statusFilter = document.createElement("wpd-segmented");
8886 statusFilter.setAttribute("value", "");
8887 const statusOptions = [
8888 { value: "", label: __("All", "desktop-mode") },
8889 { value: "active", label: __("Active", "desktop-mode") },
8890 { value: "inactive", label: __("Inactive", "desktop-mode") },
8891 { value: "update", label: __("Update available", "desktop-mode") }
8892 ];
8893 let updateCountBadge = null;
8894 for (const opt of statusOptions) {
8895 const seg = document.createElement("wpd-segment");
8896 seg.setAttribute("value", opt.value);
8897 if (opt.value === "update") {
8898 const label = document.createElement("span");
8899 label.textContent = opt.label;
8900 seg.appendChild(label);
8901 const badge = document.createElement("wpd-badge");
8902 badge.setAttribute("tone", "warning");
8903 badge.setAttribute("no-dot", "");
8904 badge.style.cssText = "margin-inline-start:6px;";
8905 badge.hidden = true;
8906 seg.appendChild(badge);
8907 updateCountBadge = badge;
8908 } else {
8909 seg.textContent = opt.label;
8910 }
8911 statusFilter.appendChild(seg);
8912 }
8913 statusFilter.addEventListener("wpd-pick", (ev) => {
8914 const detail = ev.detail;
8915 state.statusFilter = detail?.value ?? "";
8916 table.clearSelection();
8917 paintTable();
8918 });
8919 const search = document.createElement("wpd-text-field");
8920 search.setAttribute(
8921 "placeholder",
8922 __("Search installed plugins…", "desktop-mode")
8923 );
8924 let searchDebounce;
8925 search.addEventListener("wpd-input-change", (ev) => {
8926 const value = ev.detail?.value ?? "";
8927 window.clearTimeout(searchDebounce);
8928 searchDebounce = window.setTimeout(() => {
8929 state.search = value;
8930 table.clearSelection();
8931 paintTable();
8932 }, 200);
8933 });
8934 left.append(statusFilter, search);
8935 const right = document.createElement("div");
8936 right.className = "desktop-mode-plugins__toolbar-right";
8937 const bulkBar = document.createElement("div");
8938 bulkBar.className = "desktop-mode-plugins__bulk";
8939 bulkBar.hidden = true;
8940 right.appendChild(bulkBar);
8941 const trailing = document.createElement("div");
8942 trailing.className = "desktop-mode-plugins__toolbar-trailing";
8943 const refreshButton = document.createElement("wpd-button");
8944 refreshButton.setAttribute("variant", "ghost");
8945 refreshButton.setAttribute("title", __("Refresh", "desktop-mode"));
8946 refreshButton.innerHTML = '<span class="dashicons dashicons-update" aria-hidden="true"></span>';
8947 refreshButton.addEventListener("click", () => {
8948 void (async () => {
8949 await reload({ force: true });
8950 void refreshFrameworkMenu();
8951 })();
8952 });
8953 trailing.appendChild(refreshButton);
8954 toolbar.append(left, right, trailing);
8955 const tableWrap = document.createElement("div");
8956 tableWrap.className = "desktop-mode-plugins__body";
8957 const table = document.createElement("wpd-table");
8958 table.setAttribute("selectable", "multi");
8959 table.setAttribute("sticky-header", "");
8960 table.setAttribute("sticky-columns", "1");
8961 table.setAttribute("hover", "");
8962 table.setAttribute("striped", "");
8963 table.setAttribute("bordered", "");
8964 table.setAttribute("loading", "");
8965 table.setAttribute("data-installed-rows", "");
8966 const empty = document.createElement("div");
8967 empty.setAttribute("slot", "empty");
8968 empty.className = "desktop-mode-plugins__empty";
8969 empty.innerHTML = '<span class="dashicons dashicons-admin-plugins" aria-hidden="true"></span><p>' + __("No plugins match your filters.", "desktop-mode") + "</p>";
8970 table.appendChild(empty);
8971 const getRowId = (row, index) => row.plugin || String(index);
8972 table.getRowId = getRowId;
8973 table.columns = buildColumns();
8974 table.subTable = (row) => buildInstalledDetail(row);
8975 table.addEventListener("wpd-table-row-click", (ev) => {
8976 const detail = ev.detail;
8977 if (!detail) {
8978 return;
8979 }
8980 if (table.isExpanded(detail.index)) {
8981 table.collapse(detail.index);
8982 } else {
8983 table.expand(detail.index);
8984 }
8985 });
8986 tableWrap.appendChild(table);
8987 host.append(toolbar, tableWrap);
8988 const selectionListener = (ev) => {
8989 const detail = ev.detail;
8990 const ids = detail?.selection ?? [];
8991 paintBulkBar(ids);
8992 };
8993 table.addEventListener("wpd-table-selection-change", selectionListener);
8994 void reload();
8995 function buildColumns() {
8996 const cfg = getConfig();
8997 const cols = [
8998 {
8999 key: "name",
9000 label: __("Plugin", "desktop-mode"),
9001 sortable: true,
9002 sticky: true,
9003 render: (_value, row) => renderNameCell(row)
9004 },
9005 {
9006 key: "status",
9007 label: __("Status", "desktop-mode"),
9008 sortable: true,
9009 render: (_value, row) => renderStatusCell(row)
9010 },
9011 {
9012 key: "version",
9013 label: __("Version", "desktop-mode"),
9014 sortable: true,
9015 render: (_value, row) => renderVersionCell(row)
9016 },
9017 {
9018 key: "author",
9019 label: __("Author", "desktop-mode"),
9020 render: (_value, row) => renderAuthorCell(row)
9021 },
9022 {
9023 key: "desktop_mode_size_kb",
9024 label: __("Size", "desktop-mode"),
9025 align: "end",
9026 sortable: true,
9027 sortValue: (row) => row.desktop_mode_size_kb ?? 0,
9028 render: (_value, row) => formatSize(row.desktop_mode_size_kb ?? null)
9029 }
9030 ];
9031 if (cfg.autoUpdatesEnabled) {
9032 cols.push({
9033 key: "auto_updates",
9034 label: __("Automatic Updates", "desktop-mode"),
9035 sortable: true,
9036 sortValue: (row) => row.desktop_mode_auto_update?.enabled ? 1 : 0,
9037 render: (_value, row) => renderAutoUpdateCell(row)
9038 });
9039 }
9040 cols.push({
9041 key: "_actions",
9042 label: "",
9043 align: "end",
9044 render: (_value, row) => renderActionsCell(row)
9045 });
9046 return cfg.caps.activate || cfg.caps.delete ? cols : cols.slice(0, -1);
9047 }
9048 function renderNameCell(row) {
9049 const wrap = document.createElement("div");
9050 wrap.style.cssText = "display:flex;align-items:center;gap:12px;min-width:0;padding:4px 0;";
9051 const icon = document.createElement("div");
9052 icon.style.cssText = "flex:0 0 32px;width:32px;height:32px;max-width:32px;max-height:32px;border-radius:6px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.04);box-sizing:border-box;";
9053 const url = row.desktop_mode_icon_url;
9054 if (url) {
9055 const img = document.createElement("img");
9056 img.alt = "";
9057 img.loading = "lazy";
9058 img.decoding = "async";
9059 img.style.cssText = "width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;display:block;";
9060 img.src = attachIconFallback(img, url, () => {
9061 icon.replaceChildren(buildFallbackIcon());
9062 });
9063 icon.appendChild(img);
9064 } else {
9065 icon.appendChild(buildFallbackIcon());
9066 }
9067 const text = document.createElement("div");
9068 text.style.cssText = "display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto;line-height:1.35;";
9069 const title = document.createElement("strong");
9070 title.textContent = row.name || row.plugin;
9071 title.style.cssText = "display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
9072 const path = document.createElement("span");
9073 path.textContent = row.plugin;
9074 path.style.cssText = "display:block;font-size:0.78em;color:#888;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
9075 text.append(title, path);
9076 wrap.append(icon, text);
9077 return wrap;
9078 }
9079 function buildFallbackIcon() {
9080 const fallback = document.createElement("span");
9081 fallback.className = "dashicons dashicons-admin-plugins";
9082 fallback.setAttribute("aria-hidden", "true");
9083 fallback.style.cssText = "font-size:18px;width:18px;height:18px;line-height:18px;color:#888;";
9084 return fallback;
9085 }
9086 function renderStatusCell(row) {
9087 const badge = document.createElement("span");
9088 const isActive = row.status === "active" || row.status === "active-network";
9089 const dot = isActive ? "#16a34a" : "#9ca3af";
9090 const bg = isActive ? "rgba(22, 163, 74, 0.14)" : "rgba(120, 120, 120, 0.12)";
9091 const fg = isActive ? "#166e37" : "#555";
9092 badge.style.cssText = `display:inline-flex;align-items:center;gap:6px;padding:2px 10px 2px 8px;border-radius:999px;font-size:0.78em;font-weight:600;line-height:1.4;white-space:nowrap;background:${bg};color:${fg};`;
9093 const dotEl = document.createElement("span");
9094 dotEl.style.cssText = `width:6px;height:6px;border-radius:50%;background:${dot};flex:0 0 auto;display:inline-block;`;
9095 badge.appendChild(dotEl);
9096 const label = document.createElement("span");
9097 label.textContent = isActive ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode");
9098 badge.appendChild(label);
9099 return badge;
9100 }
9101 function renderVersionCell(row) {
9102 const wrap = document.createElement("div");
9103 wrap.style.cssText = "display:flex;align-items:center;gap:6px;flex-wrap:wrap;";
9104 const v = document.createElement("span");
9105 v.textContent = row.version ?? "";
9106 wrap.appendChild(v);
9107 const update = row.desktop_mode_update_available;
9108 if (update?.available && update.new_version) {
9109 const badge = document.createElement("span");
9110 badge.style.cssText = "font-size:0.78em;background:rgba(245,175,0,0.18);color:#915f00;padding:1px 7px;border-radius:999px;font-weight:600;";
9111 badge.textContent = sprintf(
9112 /* translators: %s: new plugin version */
9113 __("→ %s", "desktop-mode"),
9114 update.new_version
9115 );
9116 wrap.appendChild(badge);
9117 }
9118 return wrap;
9119 }
9120 function renderAuthorCell(row) {
9121 const wrap = document.createElement("span");
9122 wrap.style.cssText = "white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;";
9123 const text = stripHtml(row.author ?? "");
9124 wrap.textContent = text || __("Unknown", "desktop-mode");
9125 return wrap;
9126 }
9127 function renderAutoUpdateCell(row) {
9128 const wrap = document.createElement("div");
9129 wrap.setAttribute("data-noclick", "");
9130 wrap.style.cssText = "display:inline-flex;align-items:center;gap:6px;white-space:nowrap;";
9131 const meta = row.desktop_mode_auto_update;
9132 const forced = meta?.forced ?? null;
9133 if (forced !== null) {
9134 const label2 = document.createElement("span");
9135 label2.style.cssText = "color:var(--wp-desktop-text-muted,#666);";
9136 label2.textContent = forced ? __("Auto-updates enabled", "desktop-mode") : __("Auto-updates disabled", "desktop-mode");
9137 wrap.appendChild(label2);
9138 return wrap;
9139 }
9140 const supported = !!meta?.supported;
9141 if (!supported) {
9142 const placeholder = document.createElement("span");
9143 placeholder.style.cssText = "color:var(--wp-desktop-text-muted,#9ca3af);";
9144 placeholder.textContent = "";
9145 placeholder.title = __(
9146 "This plugin does not check in with WordPress.org, so automatic updates can't be scheduled.",
9147 "desktop-mode"
9148 );
9149 wrap.appendChild(placeholder);
9150 return wrap;
9151 }
9152 const enabled = !!meta?.enabled;
9153 const busy = state.autoUpdating.has(row.plugin);
9154 const link = document.createElement("a");
9155 link.href = "#";
9156 link.setAttribute("role", "button");
9157 link.setAttribute("data-wp-action", enabled ? "disable" : "enable");
9158 link.style.cssText = "display:inline-flex;align-items:center;gap:6px;color:var(--wp-desktop-accent,#2271b1);text-decoration:none;cursor:pointer;font-size:0.9em;";
9159 if (busy) {
9160 link.style.opacity = "0.6";
9161 link.style.pointerEvents = "none";
9162 link.setAttribute("aria-busy", "true");
9163 }
9164 const label = document.createElement("span");
9165 if (busy) {
9166 label.textContent = enabled ? __("Disabling…", "desktop-mode") : __("Enabling…", "desktop-mode");
9167 } else {
9168 label.textContent = enabled ? __("Disable auto-updates", "desktop-mode") : __("Enable auto-updates", "desktop-mode");
9169 }
9170 link.appendChild(label);
9171 link.addEventListener("click", (e) => {
9172 e.preventDefault();
9173 e.stopPropagation();
9174 void runToggleAutoUpdate(row);
9175 });
9176 wrap.appendChild(link);
9177 return wrap;
9178 }
9179 function renderActionsCell(row) {
9180 const wrap = document.createElement("div");
9181 wrap.style.cssText = "display:inline-flex;gap:8px;align-items:center;justify-content:flex-end;flex-wrap:nowrap;";
9182 wrap.setAttribute("data-noclick", "");
9183 const can = row.desktop_mode_can_manage ?? {
9184 activate: row.status === "inactive",
9185 deactivate: row.status === "active" || row.status === "active-network",
9186 delete: row.status === "inactive"
9187 };
9188 const update = row.desktop_mode_update_available;
9189 if (getConfig().caps.update && update?.available) {
9190 if (update.package) {
9191 const updating = state.updating.has(row.plugin);
9192 const label = updating ? __("Updating…", "desktop-mode") : sprintf(
9193 /* translators: %s: new plugin version (e.g. "1.4.2") */
9194 __("Update to %s", "desktop-mode"),
9195 update.new_version ?? ""
9196 );
9197 const btn = button2(label, "primary");
9198 if (updating) {
9199 btn.setAttribute("disabled", "");
9200 btn.setAttribute("aria-busy", "true");
9201 }
9202 btn.addEventListener("click", (e) => {
9203 e.stopPropagation();
9204 void runUpdate(row);
9205 });
9206 wrap.appendChild(btn);
9207 } else {
9208 const hint = document.createElement("span");
9209 hint.style.cssText = "font-size:0.78em;color:var(--wp-desktop-text-muted,#666);";
9210 hint.textContent = __("Auto-update unavailable", "desktop-mode");
9211 hint.title = __(
9212 "This plugin does not ship a wp.org download package. Update it manually from its source.",
9213 "desktop-mode"
9214 );
9215 wrap.appendChild(hint);
9216 }
9217 }
9218 if (can.activate) {
9219 const btn = button2(__("Activate", "desktop-mode"), "primary");
9220 btn.addEventListener("click", (e) => {
9221 e.stopPropagation();
9222 void runActivate(row);
9223 });
9224 wrap.appendChild(btn);
9225 } else if (can.deactivate) {
9226 const btn = button2(__("Deactivate", "desktop-mode"), "secondary");
9227 btn.addEventListener("click", (e) => {
9228 e.stopPropagation();
9229 void runDeactivate(row);
9230 });
9231 wrap.appendChild(btn);
9232 }
9233 if (can.delete) {
9234 const btn = button2(__("Delete", "desktop-mode"), "danger");
9235 btn.addEventListener("click", (e) => {
9236 e.stopPropagation();
9237 void runDelete(row);
9238 });
9239 wrap.appendChild(btn);
9240 }
9241 return wrap;
9242 }
9243 function button2(label, variant) {
9244 const b = document.createElement("wpd-button");
9245 b.setAttribute("variant", variant);
9246 b.setAttribute("size", "small");
9247 b.textContent = label;
9248 return b;
9249 }
9250 function paintBulkBar(ids) {
9251 bulkBar.replaceChildren();
9252 if (ids.length === 0) {
9253 bulkBar.hidden = true;
9254 return;
9255 }
9256 bulkBar.hidden = false;
9257 const count = document.createElement("span");
9258 count.className = "desktop-mode-plugins__bulk-count";
9259 count.textContent = sprintf(
9260 /* translators: %d: number of selected plugins */
9261 __("%d selected", "desktop-mode"),
9262 ids.length
9263 );
9264 bulkBar.appendChild(count);
9265 const cfg = getConfig();
9266 const selected = state.rows.filter((r) => ids.includes(r.plugin));
9267 if (cfg.caps.update) {
9268 const updatable = selected.filter(
9269 (r) => !!r.desktop_mode_update_available?.available && !!r.desktop_mode_update_available.package
9270 );
9271 if (updatable.length > 0) {
9272 const btn = button2(
9273 sprintf(
9274 /* translators: %d: number of plugins with pending updates */
9275 __("Update %d", "desktop-mode"),
9276 updatable.length
9277 ),
9278 "primary"
9279 );
9280 btn.addEventListener("click", () => {
9281 void runBulk(updatable, "update");
9282 });
9283 bulkBar.appendChild(btn);
9284 }
9285 }
9286 if (cfg.caps.activate) {
9287 const activatable = selected.filter((r) => r.status === "inactive");
9288 if (activatable.length > 0) {
9289 const btn = button2(__("Activate", "desktop-mode"), "primary");
9290 btn.addEventListener("click", () => {
9291 void runBulk(activatable, "activate");
9292 });
9293 bulkBar.appendChild(btn);
9294 }
9295 const deactivatable = selected.filter(
9296 (r) => r.status === "active" || r.status === "active-network"
9297 );
9298 if (deactivatable.length > 0) {
9299 const btn = button2(__("Deactivate", "desktop-mode"), "secondary");
9300 btn.addEventListener("click", () => {
9301 void runBulk(deactivatable, "deactivate");
9302 });
9303 bulkBar.appendChild(btn);
9304 }
9305 }
9306 if (cfg.caps.delete) {
9307 const deletable = selected.filter((r) => r.status === "inactive");
9308 if (deletable.length > 0) {
9309 const btn = button2(__("Delete", "desktop-mode"), "danger");
9310 btn.addEventListener("click", () => {
9311 void runBulk(deletable, "delete");
9312 });
9313 bulkBar.appendChild(btn);
9314 }
9315 }
9316 }
9317 async function reload(opts = {}) {
9318 state.loading = true;
9319 table.setAttribute("loading", "");
9320 try {
9321 state.rows = await fetchInstalledPlugins(opts);
9322 } catch (err) {
9323 toast(
9324 sprintf(
9325 /* translators: %s: error message */
9326 __("Could not load plugins: %s", "desktop-mode"),
9327 describe(err)
9328 ),
9329 6e3
9330 );
9331 state.rows = [];
9332 }
9333 state.loading = false;
9334 paintTable();
9335 }
9336 function paintTable() {
9337 if (state.loading) {
9338 table.setAttribute("loading", "");
9339 } else {
9340 table.removeAttribute("loading");
9341 }
9342 table.data = filterRows(state.rows);
9343 paintUpdateCount();
9344 paintBulkBar(
9345 Array.from(table.selection ?? []).map(String)
9346 );
9347 }
9348 function paintUpdateCount() {
9349 if (!updateCountBadge) {
9350 return;
9351 }
9352 const count = state.rows.filter(
9353 (r) => !!r.desktop_mode_update_available?.available
9354 ).length;
9355 if (count > 0) {
9356 updateCountBadge.textContent = String(count);
9357 updateCountBadge.hidden = false;
9358 } else {
9359 updateCountBadge.hidden = true;
9360 updateCountBadge.textContent = "";
9361 }
9362 }
9363 function filterRows(rows) {
9364 const q = state.search.trim().toLowerCase();
9365 const status = state.statusFilter;
9366 return rows.filter((row) => {
9367 if (status === "active") {
9368 if (row.status !== "active" && row.status !== "active-network") {
9369 return false;
9370 }
9371 } else if (status === "inactive") {
9372 if (row.status !== "inactive") {
9373 return false;
9374 }
9375 } else if (status === "update") {
9376 if (!row.desktop_mode_update_available?.available) {
9377 return false;
9378 }
9379 }
9380 if (q !== "") {
9381 const haystack = `${row.name ?? ""} ${row.plugin} ${stripHtml(row.author ?? "")}`.toLowerCase();
9382 if (!haystack.includes(q)) {
9383 return false;
9384 }
9385 }
9386 return true;
9387 });
9388 }
9389 async function runActivate(row) {
9390 const previous = row.status;
9391 applyStatusOptimistic(row, "active");
9392 try {
9393 const updated = await activateInstalledPlugin(row);
9394 mergeRow(updated);
9395 toast(
9396 sprintf(
9397 /* translators: %s: plugin name */
9398 __("%s activated.", "desktop-mode"),
9399 row.name || row.plugin
9400 )
9401 );
9402 broadcast(PLUGINS_CHANGED_TOPIC, {
9403 source: SOURCE,
9404 plugin: row.plugin,
9405 action: "activate"
9406 });
9407 void refreshFrameworkMenu();
9408 } catch (err) {
9409 applyStatusOptimistic(row, previous);
9410 toast(
9411 sprintf(
9412 /* translators: %s: error message */
9413 __("Activation failed: %s", "desktop-mode"),
9414 describe(err)
9415 ),
9416 6e3
9417 );
9418 }
9419 }
9420 async function runDeactivate(row) {
9421 const previous = row.status;
9422 applyStatusOptimistic(row, "inactive");
9423 try {
9424 const updated = await deactivateInstalledPlugin(row);
9425 mergeRow(updated);
9426 if (isDesktopModeSelf(row.plugin)) {
9427 toast(
9428 __(
9429 "Desktop Mode deactivated. Reloading…",
9430 "desktop-mode"
9431 ),
9432 2e3
9433 );
9434 reloadOutOfDesktopMode();
9435 return;
9436 }
9437 toast(
9438 sprintf(
9439 /* translators: %s: plugin name */
9440 __("%s deactivated.", "desktop-mode"),
9441 row.name || row.plugin
9442 )
9443 );
9444 broadcast(PLUGINS_CHANGED_TOPIC, {
9445 source: SOURCE,
9446 plugin: row.plugin,
9447 action: "deactivate"
9448 });
9449 void refreshFrameworkMenu();
9450 } catch (err) {
9451 applyStatusOptimistic(row, previous);
9452 toast(
9453 sprintf(
9454 /* translators: %s: error message */
9455 __("Deactivation failed: %s", "desktop-mode"),
9456 describe(err)
9457 ),
9458 6e3
9459 );
9460 }
9461 }
9462 async function runDelete(row) {
9463 const ok = await confirm({
9464 title: __("Delete plugin?", "desktop-mode"),
9465 message: sprintf(
9466 /* translators: %s: plugin name */
9467 __(
9468 "Permanently delete %s? Its files will be removed from disk. This cannot be undone.",
9469 "desktop-mode"
9470 ),
9471 row.name || row.plugin
9472 ),
9473 confirmLabel: __("Delete", "desktop-mode"),
9474 danger: true
9475 });
9476 if (!ok) {
9477 return;
9478 }
9479 try {
9480 await deleteInstalledPlugin(row);
9481 state.rows = state.rows.filter((r) => r.plugin !== row.plugin);
9482 paintTable();
9483 if (isDesktopModeSelf(row.plugin)) {
9484 toast(
9485 __(
9486 "Desktop Mode deleted. Reloading…",
9487 "desktop-mode"
9488 ),
9489 2e3
9490 );
9491 reloadOutOfDesktopMode();
9492 return;
9493 }
9494 toast(
9495 sprintf(
9496 /* translators: %s: plugin name */
9497 __("%s deleted.", "desktop-mode"),
9498 row.name || row.plugin
9499 )
9500 );
9501 broadcast(PLUGINS_CHANGED_TOPIC, {
9502 source: SOURCE,
9503 plugin: row.plugin,
9504 action: "delete"
9505 });
9506 void refreshFrameworkMenu();
9507 } catch (err) {
9508 toast(
9509 sprintf(
9510 /* translators: %s: error message */
9511 __("Delete failed: %s", "desktop-mode"),
9512 describe(err)
9513 ),
9514 6e3
9515 );
9516 }
9517 }
9518 async function runUpdate(row) {
9519 if (state.updating.has(row.plugin)) {
9520 return;
9521 }
9522 state.updating.add(row.plugin);
9523 paintTable();
9524 try {
9525 const result = await enqueueUpdateJob(() => updateInstalledPlugin(row));
9526 mergeRow({
9527 ...row,
9528 version: result.newVersion,
9529 desktop_mode_update_available: {
9530 available: false,
9531 new_version: null,
9532 package: "",
9533 slug: row.desktop_mode_update_available?.slug ?? ""
9534 }
9535 });
9536 toast(
9537 sprintf(
9538 /* translators: 1: plugin name, 2: new version */
9539 __("%1$s updated to %2$s.", "desktop-mode"),
9540 row.name || row.plugin,
9541 result.newVersion
9542 )
9543 );
9544 broadcast(PLUGINS_CHANGED_TOPIC, {
9545 source: SOURCE,
9546 plugin: row.plugin,
9547 action: "update"
9548 });
9549 void refreshFrameworkMenu();
9550 } catch (err) {
9551 const errCode = err?.code;
9552 const errMessage = err?.message;
9553 const coreUpToDateMessage = window.wp?.i18n?.__?.("The plugin is at the latest version.");
9554 const isUpToDate = errCode === "up_to_date" || !!coreUpToDateMessage && errMessage === coreUpToDateMessage;
9555 if (isUpToDate) {
9556 mergeRow({
9557 ...row,
9558 desktop_mode_update_available: {
9559 available: false,
9560 new_version: null,
9561 package: "",
9562 slug: row.desktop_mode_update_available?.slug ?? ""
9563 }
9564 });
9565 toast(
9566 sprintf(
9567 /* translators: %s: plugin name */
9568 __("%s is already up to date.", "desktop-mode"),
9569 row.name || row.plugin
9570 )
9571 );
9572 broadcast(PLUGINS_CHANGED_TOPIC, {
9573 source: SOURCE,
9574 plugin: row.plugin,
9575 action: "update"
9576 });
9577 } else {
9578 toast(
9579 sprintf(
9580 /* translators: 1: plugin name, 2: error message */
9581 __("Update of %1$s failed: %2$s", "desktop-mode"),
9582 row.name || row.plugin,
9583 describe(err)
9584 ),
9585 6e3
9586 );
9587 void reload();
9588 }
9589 void refreshFrameworkMenu();
9590 } finally {
9591 state.updating.delete(row.plugin);
9592 paintTable();
9593 }
9594 }
9595 async function runToggleAutoUpdate(row) {
9596 if (state.autoUpdating.has(row.plugin)) {
9597 return;
9598 }
9599 const meta = row.desktop_mode_auto_update;
9600 if (!meta || meta.forced !== null || !meta.supported) {
9601 return;
9602 }
9603 const wasEnabled = meta.enabled;
9604 const nextState = wasEnabled ? "disable" : "enable";
9605 state.autoUpdating.add(row.plugin);
9606 paintTable();
9607 try {
9608 await toggleAutoUpdate(row, nextState);
9609 mergeRow({
9610 ...row,
9611 desktop_mode_auto_update: {
9612 ...meta,
9613 enabled: !wasEnabled
9614 }
9615 });
9616 toast(
9617 wasEnabled ? sprintf(
9618 /* translators: %s: plugin name */
9619 __("Auto-updates disabled for %s.", "desktop-mode"),
9620 row.name || row.plugin
9621 ) : sprintf(
9622 /* translators: %s: plugin name */
9623 __("Auto-updates enabled for %s.", "desktop-mode"),
9624 row.name || row.plugin
9625 )
9626 );
9627 broadcast(PLUGINS_CHANGED_TOPIC, {
9628 source: SOURCE,
9629 plugin: row.plugin,
9630 action: "auto-update"
9631 });
9632 } catch (err) {
9633 toast(
9634 sprintf(
9635 /* translators: 1: plugin name, 2: error message */
9636 __(
9637 "Could not toggle auto-updates for %1$s: %2$s",
9638 "desktop-mode"
9639 ),
9640 row.name || row.plugin,
9641 describe(err)
9642 ),
9643 6e3
9644 );
9645 } finally {
9646 state.autoUpdating.delete(row.plugin);
9647 paintTable();
9648 }
9649 }
9650 async function runBulk(rows, action) {
9651 if (rows.length === 0) {
9652 return;
9653 }
9654 if (action === "delete") {
9655 const ok = await confirm({
9656 title: __("Delete selected plugins?", "desktop-mode"),
9657 message: sprintf(
9658 /* translators: %d: number of plugins */
9659 __(
9660 "Permanently delete %d plugin(s)? Their files will be removed from disk. This cannot be undone.",
9661 "desktop-mode"
9662 ),
9663 rows.length
9664 ),
9665 confirmLabel: __("Delete", "desktop-mode"),
9666 danger: true
9667 });
9668 if (!ok) {
9669 return;
9670 }
9671 }
9672 let succeeded = 0;
9673 let selfMutated = false;
9674 const failures = [];
9675 for (const row of rows) {
9676 try {
9677 if (action === "activate") {
9678 mergeRow(await activateInstalledPlugin(row));
9679 } else if (action === "deactivate") {
9680 mergeRow(await deactivateInstalledPlugin(row));
9681 } else if (action === "delete") {
9682 await deleteInstalledPlugin(row);
9683 state.rows = state.rows.filter((r) => r.plugin !== row.plugin);
9684 } else if (action === "update") {
9685 state.updating.add(row.plugin);
9686 paintTable();
9687 try {
9688 const result = await enqueueUpdateJob(
9689 () => updateInstalledPlugin(row)
9690 );
9691 mergeRow({
9692 ...row,
9693 version: result.newVersion,
9694 desktop_mode_update_available: {
9695 available: false,
9696 new_version: null,
9697 package: "",
9698 slug: row.desktop_mode_update_available?.slug ?? ""
9699 }
9700 });
9701 } finally {
9702 state.updating.delete(row.plugin);
9703 }
9704 }
9705 if ((action === "deactivate" || action === "delete") && isDesktopModeSelf(row.plugin)) {
9706 selfMutated = true;
9707 }
9708 succeeded++;
9709 } catch (err) {
9710 failures.push({ row, err });
9711 }
9712 }
9713 paintTable();
9714 table.clearSelection();
9715 if (selfMutated) {
9716 toast(
9717 action === "delete" ? __("Desktop Mode deleted. Reloading…", "desktop-mode") : __("Desktop Mode deactivated. Reloading…", "desktop-mode"),
9718 2e3
9719 );
9720 reloadOutOfDesktopMode();
9721 return;
9722 }
9723 if (succeeded > 0) {
9724 broadcast(PLUGINS_CHANGED_TOPIC, {
9725 source: SOURCE,
9726 action: "bulk"
9727 });
9728 }
9729 void refreshFrameworkMenu();
9730 let noun = "";
9731 if (action === "delete") {
9732 noun = __("deleted", "desktop-mode");
9733 } else if (action === "activate") {
9734 noun = __("activated", "desktop-mode");
9735 } else if (action === "update") {
9736 noun = __("updated", "desktop-mode");
9737 } else {
9738 noun = __("deactivated", "desktop-mode");
9739 }
9740 const summary = failures.length === 0 ? sprintf(
9741 /* translators: 1: count, 2: action verb (activated, deactivated, deleted) */
9742 __("%1$d plugin(s) %2$s.", "desktop-mode"),
9743 succeeded,
9744 noun
9745 ) : sprintf(
9746 /* translators: 1: success count, 2: failure count, 3: action verb */
9747 __("%1$d %3$s, %2$d failed.", "desktop-mode"),
9748 succeeded,
9749 failures.length,
9750 noun
9751 );
9752 toast(summary, 5e3);
9753 }
9754 function applyStatusOptimistic(row, next) {
9755 row.status = next;
9756 paintTable();
9757 }
9758 function mergeRow(updated) {
9759 const idx = state.rows.findIndex((r) => r.plugin === updated.plugin);
9760 if (idx >= 0) {
9761 state.rows[idx] = { ...state.rows[idx], ...updated };
9762 } else {
9763 state.rows.push(updated);
9764 }
9765 paintTable();
9766 }
9767 const unsubscribePluginsChanged = subscribe(
9768 PLUGINS_CHANGED_TOPIC,
9769 (payload) => {
9770 if (payload?.source === SOURCE) {
9771 return;
9772 }
9773 void reload();
9774 }
9775 );
9776 return () => {
9777 unsubscribePluginsChanged();
9778 table.removeEventListener("wpd-table-selection-change", selectionListener);
9779 host.replaceChildren();
9780 };
9781 }
9782 function formatSize(kb) {
9783 if (kb === null || kb === void 0) {
9784 return "";
9785 }
9786 if (kb < 1024) {
9787 return sprintf(
9788 /* translators: %d: size in kilobytes */
9789 __("%d KB", "desktop-mode"),
9790 kb
9791 );
9792 }
9793 const mb = kb / 1024;
9794 return sprintf(
9795 /* translators: %s: size in megabytes (one decimal) */
9796 __("%s MB", "desktop-mode"),
9797 mb.toFixed(1)
9798 );
9799 }
9800 function stripHtml(html2) {
9801 const tmp = document.createElement("div");
9802 tmp.innerHTML = html2;
9803 return tmp.textContent ?? "";
9804 }
9805 function describe(err) {
9806 if (err instanceof Error) {
9807 return err.message;
9808 }
9809 return String(err);
9810 }
9811 const _initial = {
9812 tab: null,
9813 requestedAt: 0
9814 };
9815 let _store = null;
9816 function getStore() {
9817 if (_store) {
9818 return _store;
9819 }
9820 const w = window;
9821 const factory = w.wp?.desktop?.createSharedStore;
9822 if (typeof factory !== "function") {
9823 return null;
9824 }
9825 _store = factory(
9826 "desktop-mode/plugins-window/tab-target",
9827 () => ({ ..._initial })
9828 );
9829 return _store;
9830 }
9831 function consumePluginsWindowTab() {
9832 const store = getStore();
9833 if (store) {
9834 const tab = store.state.tab;
9835 if (tab !== null) {
9836 store.state.tab = null;
9837 store.state.requestedAt = 0;
9838 store.notify();
9839 }
9840 return tab;
9841 }
9842 const w = window;
9843 const prev = w._wpdPluginsWindowTab;
9844 if (prev) {
9845 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
9846 return prev.tab;
9847 }
9848 return null;
9849 }
9850 function subscribePluginsWindowTab(cb) {
9851 const store = getStore();
9852 if (!store) {
9853 return () => {
9854 };
9855 }
9856 return store.subscribe((state) => cb({ ...state }));
9857 }
9858 function renderPluginsWindow(body) {
9859 const root = body.querySelector(
9860 "[data-desktop-mode-plugins-root]"
9861 );
9862 if (!root) {
9863 body.innerHTML = '<p style="padding:20px;color:var(--wpd-fg-muted,#666);">' + __("Plugins window template missing.", "desktop-mode") + "</p>";
9864 return;
9865 }
9866 const config = getConfig();
9867 const tabs = root.querySelector(
9868 "[data-desktop-mode-plugins-tabs]"
9869 );
9870 const installedHost = root.querySelector(
9871 "[data-desktop-mode-plugins-installed-host]"
9872 );
9873 let installedTeardown = null;
9874 if (installedHost) {
9875 if (config.caps.activate) {
9876 installedTeardown = mountInstalledView(installedHost);
9877 } else {
9878 installedHost.replaceChildren();
9879 const msg = document.createElement("p");
9880 msg.style.padding = "20px";
9881 msg.style.color = "var(--wpd-fg-muted, #666)";
9882 msg.textContent = __(
9883 "You do not have permission to manage plugins.",
9884 "desktop-mode"
9885 );
9886 installedHost.appendChild(msg);
9887 }
9888 }
9889 const browseHost = root.querySelector(
9890 "[data-desktop-mode-plugins-browse-host]"
9891 );
9892 const flyout = root.querySelector(
9893 "[data-desktop-mode-plugins-flyout]"
9894 );
9895 let browseTeardown = null;
9896 if (browseHost && config.caps.install) {
9897 browseTeardown = mountBrowseView(browseHost, flyout, body);
9898 }
9899 const featuredHost = root.querySelector(
9900 "[data-desktop-mode-plugins-featured-host]"
9901 );
9902 let featuredTeardown = null;
9903 if (featuredHost && config.caps.install) {
9904 featuredTeardown = mountFeaturedView(featuredHost, flyout);
9905 }
9906 const applyTab = (tab) => {
9907 if (!tabs) {
9908 return;
9909 }
9910 if ((tab === "browse" || tab === "featured") && !config.caps.install) {
9911 tabs.setAttribute("value", "installed");
9912 return;
9913 }
9914 tabs.setAttribute("value", tab);
9915 };
9916 const initialTab = consumePluginsWindowTab();
9917 if (initialTab) {
9918 applyTab(initialTab);
9919 }
9920 const unsubscribeTab = subscribePluginsWindowTab((state) => {
9921 if (state.tab) {
9922 applyTab(state.tab);
9923 }
9924 });
9925 const onClosed = (ev) => {
9926 const detail = ev.detail;
9927 if (detail?.windowId !== "desktop-mode-plugins") {
9928 return;
9929 }
9930 document.removeEventListener("desktop-mode-window-closed", onClosed);
9931 unsubscribeTab();
9932 if (installedTeardown) {
9933 installedTeardown();
9934 installedTeardown = null;
9935 }
9936 if (browseTeardown) {
9937 browseTeardown();
9938 browseTeardown = null;
9939 }
9940 if (featuredTeardown) {
9941 featuredTeardown();
9942 featuredTeardown = null;
9943 }
9944 };
9945 document.addEventListener("desktop-mode-window-closed", onClosed);
9946 void maybeShowIntro(config);
9947 }
9948 let _introShown = false;
9949 async function maybeShowIntro(config) {
9950 if (_introShown || config.introSeen) {
9951 return;
9952 }
9953 _introShown = true;
9954 try {
9955 const { showPluginsIntroDialog: showPluginsIntroDialog2 } = await Promise.resolve().then(() => introDialog);
9956 const result = await showPluginsIntroDialog2();
9957 if (result === "cancel") {
9958 _introShown = false;
9959 return;
9960 }
9961 void markIntroSeen(config);
9962 if (result === "settings") {
9963 openOsSettingsFeatures();
9964 }
9965 } catch {
9966 _introShown = false;
9967 }
9968 }
9969 async function markIntroSeen(config) {
9970 if (!config.introUrl) {
9971 return;
9972 }
9973 try {
9974 await trackedFetch(
9975 config.introUrl,
9976 {
9977 method: "POST",
9978 credentials: "same-origin",
9979 headers: {
9980 "Content-Type": "application/json",
9981 "X-WP-Nonce": config.restNonce
9982 },
9983 body: JSON.stringify({ slug: "plugins" })
9984 },
9985 {
9986 windowId: "desktop-mode-plugins",
9987 source: "plugins-window/intro"
9988 }
9989 );
9990 config.introSeen = true;
9991 } catch {
9992 }
9993 }
9994 function openOsSettingsFeatures() {
9995 const api2 = window.wp?.desktop;
9996 if (typeof api2?.openOsSettings === "function") {
9997 api2.openOsSettings({ tabId: "features" });
9998 }
9999 }
10000 const registry = window.desktopModeNativeWindows ?? (window.desktopModeNativeWindows = {});
10001 registry["desktop-mode-plugins"] = (body) => {
10002 renderPluginsWindow(body);
10003 };
10004 async function showPluginsIntroDialog() {
10005 return new Promise((resolve) => {
10006 const backdrop = document.createElement("div");
10007 backdrop.className = "desktop-mode-plugins-intro__backdrop";
10008 backdrop.setAttribute("role", "presentation");
10009 Object.assign(backdrop.style, {
10010 position: "fixed",
10011 inset: "0",
10012 background: "color-mix(in srgb, var(--wp-admin-theme-color, #1d2327) 60%, transparent)",
10013 backdropFilter: "blur(2px)",
10014 WebkitBackdropFilter: "blur(2px)",
10015 zIndex: "100000",
10016 display: "flex",
10017 alignItems: "center",
10018 justifyContent: "center",
10019 padding: "24px"
10020 });
10021 const dialog = document.createElement("div");
10022 dialog.setAttribute("role", "dialog");
10023 dialog.setAttribute("aria-modal", "true");
10024 dialog.setAttribute("aria-labelledby", "desktop-mode-plugins-intro-title");
10025 dialog.className = "desktop-mode-plugins-intro";
10026 Object.assign(dialog.style, {
10027 background: "var(--wp-admin-theme-bg, #fff)",
10028 color: "var(--wp-admin-theme-fg, #1d2327)",
10029 borderRadius: "14px",
10030 boxShadow: "0 24px 60px rgba(0,0,0,.28)",
10031 maxWidth: "560px",
10032 width: "100%",
10033 maxHeight: "90vh",
10034 overflow: "auto",
10035 padding: "28px 32px 24px",
10036 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
10037 });
10038 dialog.innerHTML = renderDialogMarkup();
10039 backdrop.appendChild(dialog);
10040 document.body.appendChild(backdrop);
10041 const primaryBtn = dialog.querySelector(
10042 '[data-action="confirm"]'
10043 );
10044 const settingsBtn = dialog.querySelector(
10045 '[data-action="settings"]'
10046 );
10047 primaryBtn?.focus();
10048 let resolved = false;
10049 const cleanup = (result) => {
10050 if (resolved) {
10051 return;
10052 }
10053 resolved = true;
10054 document.removeEventListener("keydown", onKey, true);
10055 backdrop.remove();
10056 resolve(result);
10057 };
10058 const onKey = (e) => {
10059 if (e.key === "Escape") {
10060 e.preventDefault();
10061 cleanup("cancel");
10062 }
10063 };
10064 document.addEventListener("keydown", onKey, true);
10065 backdrop.addEventListener("click", (e) => {
10066 if (e.target === backdrop) {
10067 cleanup("cancel");
10068 }
10069 });
10070 primaryBtn?.addEventListener("click", () => cleanup("confirm"));
10071 settingsBtn?.addEventListener("click", () => cleanup("settings"));
10072 });
10073 }
10074 function renderDialogMarkup() {
10075 const title = __("Welcome to the new Plugins window", "desktop-mode");
10076 const lede = __(
10077 "You're looking at the redesigned Plugins admin — same WordPress.org repository under the hood, with a workflow tuned for how Desktop Mode wants you to work.",
10078 "desktop-mode"
10079 );
10080 const highlights = [
10081 __(
10082 "Two tabs in one window — Installed for managing what you have, Browse for discovering new plugins. No more bouncing between Plugins → Add New → back to Installed.",
10083 "desktop-mode"
10084 ),
10085 __(
10086 "A real gallery on Browse — clean cards with rating, install count, last updated, and a click-anywhere detail flyout. Subtle hover lift, lazy-loaded icons, infinite scroll.",
10087 "desktop-mode"
10088 ),
10089 __(
10090 "The detail flyout shows screenshots, the ratings histogram, recent reviews, the changelog and FAQ — all without leaving the window.",
10091 "desktop-mode"
10092 ),
10093 __(
10094 "Drag a .zip onto the window to install — or drag a Browse card straight to the dock to pin a shortcut. The framework drag bridge handles the rest.",
10095 "desktop-mode"
10096 ),
10097 __(
10098 'The dock repaints LIVE after every install / activate / deactivate / delete. No reload, no stale tile, no "wait, did that work?".',
10099 "desktop-mode"
10100 ),
10101 __(
10102 "Per-row capability flags so the UI hides actions you can't perform — and the server re-validates every mutation, so flags can't be tampered into more permissions.",
10103 "desktop-mode"
10104 )
10105 ];
10106 const li = (arr) => arr.map(
10107 (s) => `<li><span class="dot" aria-hidden="true"></span>${escapeHtml(
10108 s
10109 )}</li>`
10110 ).join("");
10111 return `
10112 <style>
10113 .desktop-mode-plugins-intro h2 {
10114 margin: 0 0 8px;
10115 font-size: 22px;
10116 font-weight: 600;
10117 letter-spacing: -0.01em;
10118 }
10119 .desktop-mode-plugins-intro p.lede {
10120 margin: 0 0 20px;
10121 color: var(--wp-admin-theme-fg-muted, #50575e);
10122 font-size: 14px;
10123 line-height: 1.5;
10124 }
10125 .desktop-mode-plugins-intro__list {
10126 list-style: none;
10127 margin: 0 0 22px;
10128 padding: 0;
10129 font-size: 14px;
10130 line-height: 1.5;
10131 }
10132 .desktop-mode-plugins-intro__list li {
10133 display: flex;
10134 align-items: flex-start;
10135 gap: 10px;
10136 padding: 6px 0;
10137 }
10138 .desktop-mode-plugins-intro__list .dot {
10139 flex: 0 0 auto;
10140 width: 6px;
10141 height: 6px;
10142 margin-top: 9px;
10143 border-radius: 50%;
10144 background: var(--wp-admin-theme-color, #2271b1);
10145 }
10146 .desktop-mode-plugins-intro__footer {
10147 display: flex;
10148 justify-content: flex-end;
10149 gap: 8px;
10150 margin-top: 8px;
10151 }
10152 .desktop-mode-plugins-intro__footer button {
10153 appearance: none;
10154 border: 1px solid var(--wp-admin-theme-border, #dcdcde);
10155 background: var(--wp-admin-theme-bg, #fff);
10156 color: inherit;
10157 padding: 8px 14px;
10158 border-radius: 6px;
10159 font-size: 13px;
10160 cursor: pointer;
10161 }
10162 .desktop-mode-plugins-intro__footer button.primary {
10163 border-color: var(--wp-admin-theme-color, #2271b1);
10164 background: var(--wp-admin-theme-color, #2271b1);
10165 color: #fff;
10166 font-weight: 500;
10167 }
10168 .desktop-mode-plugins-intro__footer button:hover {
10169 filter: brightness(1.05);
10170 }
10171 .desktop-mode-plugins-intro__footer button:focus-visible {
10172 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
10173 outline-offset: 2px;
10174 }
10175 </style>
10176 <h2 id="desktop-mode-plugins-intro-title">${escapeHtml(title)}</h2>
10177 <p class="lede">${escapeHtml(lede)}</p>
10178 <ul class="desktop-mode-plugins-intro__list">${li(highlights)}</ul>
10179 <div class="desktop-mode-plugins-intro__footer">
10180 <button type="button" data-action="settings">${escapeHtml(
10181 __("Take me to settings", "desktop-mode")
10182 )}</button>
10183 <button type="button" class="primary" data-action="confirm">${escapeHtml(
10184 __("Got it", "desktop-mode")
10185 )}</button>
10186 </div>
10187 `;
10188 }
10189 function escapeHtml(s) {
10190 const t = document.createElement("div");
10191 t.textContent = s;
10192 return t.innerHTML;
10193 }
10194 const introDialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10195 __proto__: null,
10196 showPluginsIntroDialog
10197 }, Symbol.toStringTag, { value: "Module" }));
10198 })();
10199