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

10,124 lines 350.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(/%[sd]/g, () => String(args[i++] ?? ""));
80 }
81 function html(strings, ...values) {
82 return { __wpdHtml: true, strings, values };
83 }
84 function isTemplateResult$1(v) {
85 return !!v && v.__wpdHtml === true;
86 }
87 const MARKER_PREFIX = "$$wpd$$";
88 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
89 function joinWithMarkers(strings) {
90 let out = strings[0];
91 for (let i = 1; i < strings.length; i++) {
92 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
93 }
94 return out;
95 }
96 const compiledCache = /* @__PURE__ */ new WeakMap();
97 function compile(strings) {
98 const cached = compiledCache.get(strings);
99 if (cached) {
100 return cached;
101 }
102 const template = document.createElement("template");
103 template.innerHTML = joinWithMarkers(strings);
104 const recipes = [];
105 const walk = (node, path) => {
106 if (node.nodeType === Node.ELEMENT_NODE) {
107 const el = node;
108 for (const attr of Array.from(el.attributes)) {
109 const rawName = attr.name;
110 const rawValue = attr.value;
111 const prefix = rawName[0];
112 if (MARKER_RE.test(rawValue)) {
113 MARKER_RE.lastIndex = 0;
114 if (prefix === "@") {
115 const match = MARKER_RE.exec(rawValue);
116 MARKER_RE.lastIndex = 0;
117 recipes.push({
118 path,
119 kind: "event",
120 name: rawName.slice(1),
121 valueIndex: match ? Number(match[1]) : 0
122 });
123 el.removeAttribute(rawName);
124 } else if (prefix === ".") {
125 const match = MARKER_RE.exec(rawValue);
126 MARKER_RE.lastIndex = 0;
127 recipes.push({
128 path,
129 kind: "prop",
130 name: rawName.slice(1),
131 valueIndex: match ? Number(match[1]) : 0
132 });
133 el.removeAttribute(rawName);
134 } else if (prefix === "?") {
135 const match = MARKER_RE.exec(rawValue);
136 MARKER_RE.lastIndex = 0;
137 recipes.push({
138 path,
139 kind: "bool",
140 name: rawName.slice(1),
141 valueIndex: match ? Number(match[1]) : 0
142 });
143 el.removeAttribute(rawName);
144 } else {
145 const fragments = [];
146 const indices = [];
147 let lastEnd = 0;
148 let m;
149 MARKER_RE.lastIndex = 0;
150 while ((m = MARKER_RE.exec(rawValue)) !== null) {
151 fragments.push(rawValue.slice(lastEnd, m.index));
152 indices.push(Number(m[1]));
153 lastEnd = m.index + m[0].length;
154 }
155 fragments.push(rawValue.slice(lastEnd));
156 recipes.push({
157 path,
158 kind: "attr",
159 name: rawName,
160 template: fragments,
161 valueIndices: indices
162 });
163 el.setAttribute(rawName, "");
164 }
165 }
166 }
167 }
168 const children = Array.from(node.childNodes);
169 let shift = 0;
170 for (let i = 0; i < children.length; i++) {
171 const child = children[i];
172 const liveIndex = i + shift;
173 if (child.nodeType === Node.TEXT_NODE) {
174 const text = child.textContent || "";
175 if (!MARKER_RE.test(text)) {
176 MARKER_RE.lastIndex = 0;
177 continue;
178 }
179 MARKER_RE.lastIndex = 0;
180 const parent = child.parentNode;
181 let lastEnd = 0;
182 let m;
183 const newNodes = [];
184 const newRecipes = [];
185 MARKER_RE.lastIndex = 0;
186 while ((m = MARKER_RE.exec(text)) !== null) {
187 if (m.index > lastEnd) {
188 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
189 }
190 const placeholder = document.createTextNode("");
191 newNodes.push(placeholder);
192 newRecipes.push({
193 path: [...path, liveIndex + newNodes.length - 1],
194 kind: "node",
195 valueIndex: Number(m[1])
196 });
197 lastEnd = m.index + m[0].length;
198 }
199 if (lastEnd < text.length) {
200 newNodes.push(document.createTextNode(text.slice(lastEnd)));
201 }
202 for (const nn of newNodes) {
203 parent.insertBefore(nn, child);
204 }
205 parent.removeChild(child);
206 shift += newNodes.length - 1;
207 recipes.push(...newRecipes);
208 } else {
209 walk(child, [...path, liveIndex]);
210 }
211 }
212 };
213 walk(template.content, []);
214 const buildParts = (fragment) => {
215 const out = [];
216 for (const r of recipes) {
217 let node = fragment;
218 for (const idx of r.path) {
219 node = node.childNodes[idx];
220 }
221 if (r.kind === "node") {
222 out.push({
223 kind: "node",
224 valueIndex: r.valueIndex,
225 child: {
226 anchor: node,
227 state: null
228 }
229 });
230 } else if (r.kind === "attr") {
231 out.push({
232 kind: "attr",
233 element: node,
234 name: r.name,
235 template: r.template,
236 valueIndices: r.valueIndices
237 });
238 } else if (r.kind === "event") {
239 out.push({
240 kind: "event",
241 valueIndex: r.valueIndex,
242 element: node,
243 name: r.name
244 });
245 } else if (r.kind === "prop") {
246 out.push({
247 kind: "prop",
248 valueIndex: r.valueIndex,
249 element: node,
250 name: r.name
251 });
252 } else if (r.kind === "bool") {
253 out.push({
254 kind: "bool",
255 valueIndex: r.valueIndex,
256 element: node,
257 name: r.name
258 });
259 }
260 }
261 return out;
262 };
263 const entry = { template, buildParts };
264 compiledCache.set(strings, entry);
265 return entry;
266 }
267 const mountState = /* @__PURE__ */ new WeakMap();
268 function render(result, container) {
269 const existing = mountState.get(container);
270 if (existing && existing.strings === result.strings) {
271 applyValues(existing.parts, result.values);
272 return;
273 }
274 const compiled = compile(result.strings);
275 const fragment = compiled.template.content.cloneNode(true);
276 const parts = compiled.buildParts(fragment);
277 while (container.firstChild) {
278 container.removeChild(container.firstChild);
279 }
280 container.appendChild(fragment);
281 applyValues(parts, result.values);
282 mountState.set(container, { strings: result.strings, parts });
283 }
284 function applyValues(parts, values) {
285 for (const part of parts) {
286 if (part.kind === "node") {
287 updateChildPart(part.child, values[part.valueIndex]);
288 } else if (part.kind === "attr") {
289 let composed = part.template[0];
290 for (let i = 0; i < part.valueIndices.length; i++) {
291 composed += formatText(values[part.valueIndices[i]]);
292 composed += part.template[i + 1];
293 }
294 if (composed !== part.last) {
295 part.last = composed;
296 if (composed === "") {
297 part.element.removeAttribute(part.name);
298 } else {
299 part.element.setAttribute(part.name, composed);
300 }
301 }
302 } else if (part.kind === "event") {
303 const next = values[part.valueIndex];
304 if (next !== part.current) {
305 if (part.current) {
306 part.element.removeEventListener(part.name, part.current);
307 }
308 if (next) {
309 part.element.addEventListener(part.name, next);
310 }
311 part.current = next;
312 }
313 } else if (part.kind === "prop") {
314 const next = values[part.valueIndex];
315 if (next !== part.last) {
316 part.last = next;
317 part.element[part.name] = next;
318 }
319 } else if (part.kind === "bool") {
320 const next = !!values[part.valueIndex];
321 if (next !== part.last) {
322 part.last = next;
323 if (next) {
324 part.element.setAttribute(part.name, "");
325 } else {
326 part.element.removeAttribute(part.name);
327 }
328 }
329 }
330 }
331 }
332 function updateChildPart(child, value) {
333 if (value === null || value === void 0 || value === false) {
334 if (child.state) {
335 disposeChildState(child.state);
336 child.state = null;
337 }
338 return;
339 }
340 if (Array.isArray(value)) {
341 updateArrayChild(child, value);
342 return;
343 }
344 if (isTemplateResult$1(value)) {
345 updateTemplateChild(child, value);
346 return;
347 }
348 if (value instanceof Node) {
349 updateNodeChild(child, value);
350 return;
351 }
352 updateTextChild(child, formatText(value));
353 }
354 function updateNodeChild(child, node) {
355 const old = child.state;
356 if (old?.shape === "node" && old.node === node) {
357 return;
358 }
359 if (old) {
360 disposeChildState(old);
361 }
362 insertBeforeAnchor(child, [node]);
363 child.state = { shape: "node", node };
364 }
365 function updateTextChild(child, text) {
366 const old = child.state;
367 if (old?.shape === "text") {
368 if (old.text !== text) {
369 old.node.textContent = text;
370 old.text = text;
371 }
372 return;
373 }
374 if (old) {
375 disposeChildState(old);
376 }
377 const node = document.createTextNode(text);
378 insertBeforeAnchor(child, [node]);
379 child.state = { shape: "text", node, text };
380 }
381 function updateTemplateChild(child, result) {
382 const old = child.state;
383 if (old?.shape === "template" && old.strings === result.strings) {
384 applyValues(old.parts, result.values);
385 return;
386 }
387 if (old) {
388 disposeChildState(old);
389 }
390 const compiled = compile(result.strings);
391 const fragment = compiled.template.content.cloneNode(true);
392 const parts = compiled.buildParts(fragment);
393 const topNodes = Array.from(fragment.childNodes);
394 insertBeforeAnchor(child, [fragment]);
395 applyValues(parts, result.values);
396 child.state = {
397 shape: "template",
398 strings: result.strings,
399 parts,
400 nodes: topNodes
401 };
402 }
403 function updateArrayChild(child, arr) {
404 const old = child.state;
405 if (old?.shape === "array" && old.entries.length === arr.length) {
406 for (let i = 0; i < arr.length; i++) {
407 updateChildPart(old.entries[i], arr[i]);
408 }
409 return;
410 }
411 if (old) {
412 disposeChildState(old);
413 }
414 const entries = [];
415 for (const v of arr) {
416 const entryAnchor = document.createTextNode("");
417 insertBeforeAnchor(child, [entryAnchor]);
418 const entry = { anchor: entryAnchor, state: null };
419 updateChildPart(entry, v);
420 entries.push(entry);
421 }
422 child.state = { shape: "array", entries };
423 }
424 function insertBeforeAnchor(child, nodes) {
425 const parent = child.anchor.parentNode;
426 if (!parent) {
427 return;
428 }
429 for (const node of nodes) {
430 parent.insertBefore(node, child.anchor);
431 }
432 }
433 function disposeChildState(state) {
434 if (state.shape === "text") {
435 state.node.remove();
436 return;
437 }
438 if (state.shape === "template") {
439 for (const node of state.nodes) {
440 if (node.parentNode) {
441 node.parentNode.removeChild(node);
442 }
443 }
444 return;
445 }
446 if (state.shape === "node") {
447 if (state.node.parentNode) {
448 state.node.parentNode.removeChild(state.node);
449 }
450 return;
451 }
452 for (const entry of state.entries) {
453 if (entry.state) {
454 disposeChildState(entry.state);
455 }
456 entry.anchor.remove();
457 }
458 }
459 function formatText(v) {
460 if (v === null || v === void 0 || v === false) {
461 return "";
462 }
463 return String(v);
464 }
465 const _Component = class _Component extends HTMLElement {
466 constructor() {
467 super();
468 this._renderScheduled = false;
469 this._propValues = {};
470 const ctor = this.constructor;
471 if (ctor.shadow) {
472 this.attachShadow({ mode: "open" });
473 this._renderRoot = this.shadowRoot;
474 } else {
475 this._renderRoot = this;
476 }
477 this._installPropAccessors();
478 }
479 static get observedAttributes() {
480 return this.props.map(kebab);
481 }
482 connectedCallback() {
483 this._adoptStyles();
484 this.requestUpdate();
485 }
486 attributeChangedCallback(name, oldValue, newValue) {
487 if (oldValue === newValue) {
488 return;
489 }
490 const prop = camel(name);
491 this._propValues[prop] = newValue;
492 this.requestUpdate();
493 }
494 /**
495 * Declarative class-name setter. Assign an array (or a
496 * space-separated string) and the host's `class` attribute is
497 * rewritten to match. Intended for programmatic styling — when
498 * a plugin has enqueued its own stylesheet and wants to apply
499 * one of those classes to a shell component:
500 *
501 * ```js
502 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
503 * // → <wpd-select class="my-plugin-brand is-active">
504 * ```
505 *
506 * The plain HTML `class="…"` attribute works just the same and
507 * is always preferred when writing markup by hand — this setter
508 * exists for the JS-API case where the caller has an array of
509 * conditional classes in hand.
510 *
511 * Getter returns the current `classList` as a plain array for
512 * symmetric read/write.
513 *
514 * @since 0.13.0
515 */
516 get classNames() {
517 return Array.from(this.classList);
518 }
519 set classNames(next) {
520 if (next === null || next === void 0) {
521 this.removeAttribute("class");
522 return;
523 }
524 const list = Array.isArray(next) ? next : String(next).split(/\s+/);
525 const cleaned = list.map((s) => String(s).trim()).filter((s) => s !== "");
526 this.className = cleaned.join(" ");
527 }
528 /**
529 * Request a re-render explicitly. Components rarely need this —
530 * declare state via props + attribute observers and the render
531 * loop picks up changes automatically.
532 */
533 requestUpdate() {
534 this._scheduleRender();
535 }
536 /**
537 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
538 * by default (matches typical WC UX — events cross shadow
539 * boundaries, parents can listen without knowing about internal
540 * structure).
541 */
542 emit(name, detail) {
543 return this.dispatchEvent(
544 new CustomEvent(name, {
545 detail,
546 bubbles: true,
547 composed: true
548 })
549 );
550 }
551 // ------------------------------------------------------------------
552 // Internals
553 // ------------------------------------------------------------------
554 /**
555 * Wire every `static props` entry to a matched property getter +
556 * setter on the element. Setting the property reflects into the
557 * attribute (so downstream observers + CSS selectors see it);
558 * reading the property falls back to the attribute.
559 */
560 _installPropAccessors() {
561 const ctor = this.constructor;
562 for (const prop of ctor.props) {
563 if (Object.getOwnPropertyDescriptor(this, prop)) {
564 continue;
565 }
566 const attr = kebab(prop);
567 Object.defineProperty(this, prop, {
568 get: () => {
569 if (prop in this._propValues) {
570 return this._propValues[prop];
571 }
572 return this.getAttribute(attr);
573 },
574 set: (value) => {
575 let str;
576 if (value === null || value === void 0 || value === false) {
577 str = null;
578 } else if (value === true) {
579 str = "";
580 } else {
581 str = String(value);
582 }
583 this._propValues[prop] = str;
584 if (str === null) {
585 this.removeAttribute(attr);
586 } else {
587 this.setAttribute(attr, str);
588 }
589 this.requestUpdate();
590 },
591 enumerable: true,
592 configurable: true
593 });
594 }
595 }
596 /**
597 * Schedule a render on the next microtask. Multiple property
598 * assignments in the same tick collapse into a single render.
599 */
600 _scheduleRender() {
601 if (this._renderScheduled || !this.isConnected) {
602 return;
603 }
604 this._renderScheduled = true;
605 queueMicrotask(() => {
606 this._renderScheduled = false;
607 if (!this.isConnected) {
608 return;
609 }
610 render(this.render(), this._renderRoot);
611 });
612 }
613 /**
614 * Mount adoptable stylesheets onto the shadow root (via
615 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
616 * tag per def). No-op if `static styles` is empty.
617 */
618 _adoptStyles() {
619 const ctor = this.constructor;
620 if (ctor.styles.length === 0) {
621 return;
622 }
623 if (ctor.shadow && this.shadowRoot) {
624 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
625 this.shadowRoot.adoptedStyleSheets = sheets;
626 if (sheets.length !== ctor.styles.length) {
627 for (const s of ctor.styles) {
628 if (!s.sheet) {
629 const tag = document.createElement("style");
630 tag.textContent = s.cssText;
631 this.shadowRoot.appendChild(tag);
632 }
633 }
634 }
635 } else {
636 this._adoptLightStyles(ctor);
637 }
638 }
639 _adoptLightStyles(ctor) {
640 if (_Component._lightStylesAdopted.has(ctor)) {
641 return;
642 }
643 _Component._lightStylesAdopted.add(ctor);
644 for (const s of ctor.styles) {
645 const tag = document.createElement("style");
646 tag.dataset.wpdUi = this.tagName.toLowerCase();
647 tag.textContent = s.cssText;
648 document.head.appendChild(tag);
649 }
650 }
651 };
652 _Component.props = [];
653 _Component.styles = [];
654 _Component.shadow = true;
655 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
656 let Component = _Component;
657 function defineComponent(tag, ctor) {
658 if (customElements.get(tag)) {
659 return;
660 }
661 customElements.define(tag, ctor);
662 }
663 function kebab(s) {
664 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
665 }
666 function camel(s) {
667 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
668 }
669 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
670 try {
671 const s = new CSSStyleSheet();
672 return typeof s.replaceSync === "function";
673 } catch {
674 return false;
675 }
676 })();
677 function css(strings, ...values) {
678 let text = strings[0];
679 for (let i = 1; i < strings.length; i++) {
680 const v = values[i - 1];
681 if (typeof v === "string" || typeof v === "number") {
682 text += String(v);
683 } else if (v && v.__wpdCss) {
684 text += v.cssText;
685 } else {
686 throw new TypeError(
687 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
688 );
689 }
690 text += strings[i];
691 }
692 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
693 const sheet = new CSSStyleSheet();
694 sheet.replaceSync(text);
695 return { __wpdCss: true, sheet, cssText: text };
696 }
697 return { __wpdCss: true, sheet: null, cssText: text };
698 }
699 function computeAutoId(element) {
700 const parts = [];
701 const tabs = [];
702 let windowId = null;
703 let node = element.parentElement;
704 while (node) {
705 if (node === document.body || node === document.documentElement) {
706 break;
707 }
708 const id = node.id || "";
709 if (id.startsWith("wp-window-")) {
710 windowId = id.slice("wp-window-".length);
711 break;
712 }
713 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
714 const forValue = node.getAttribute("for");
715 if (forValue) {
716 tabs.unshift(forValue);
717 }
718 }
719 node = node.parentElement;
720 }
721 if (windowId) {
722 parts.push(slugify(windowId));
723 }
724 for (const tab of tabs) {
725 parts.push("tab-" + slugify(tab));
726 }
727 const label = element.getAttribute("label");
728 if (label) {
729 parts.push(slugify(label));
730 }
731 if (parts.length === 0) {
732 return "wpd-unnamed";
733 }
734 return "wpd-" + parts.filter((p) => p !== "").join("-");
735 }
736 function slugify(s) {
737 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
738 }
739 function ensureAutoId(element) {
740 if (element.id) {
741 return element.id;
742 }
743 const id = computeAutoId(element);
744 element.id = id;
745 return id;
746 }
747 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}}`;
748 const EXPANDER_KEY = "__wpd_expander__";
749 const SELECT_KEY = "__wpd_select__";
750 const _WpdTable = class _WpdTable extends Component {
751 constructor() {
752 super(...arguments);
753 this._data = [];
754 this._columns = [];
755 this._filters = {};
756 this._expanded = /* @__PURE__ */ new Set();
757 this._subTable = null;
758 this._sort = null;
759 this._selection = /* @__PURE__ */ new Set();
760 this._getRowId = (_row, index) => index;
761 this._filterCache = /* @__PURE__ */ new Map();
762 this._paintScheduled = false;
763 this._stickyHeaderWarned = false;
764 this._stickyRaceWarned = false;
765 this._resizeObserver = null;
766 this._stickyMicroScheduled = false;
767 this._stickyRafHandle = null;
768 this._loadingDesyncWarned = false;
769 this._lastStickyIndex = -1;
770 }
771 // ------------------------------------------------------------------
772 // Public properties — set from JS (use `.data=${...}` in templates).
773 // ------------------------------------------------------------------
774 /** The row buffer. Reassigning replaces (and clears expansion state). */
775 get data() {
776 return this._data;
777 }
778 set data(next) {
779 this._data = Array.isArray(next) ? next.slice() : [];
780 this._expanded.clear();
781 this._schedulePaint();
782 }
783 /** Column descriptors. See {@link WpdTableColumn}. */
784 get columns() {
785 return this._columns;
786 }
787 set columns(next) {
788 this._columns = Array.isArray(next) ? next.slice() : [];
789 const keys = new Set(this._columns.map((c) => c.key));
790 for (const k of Object.keys(this._filters)) {
791 if (!keys.has(k)) {
792 delete this._filters[k];
793 }
794 }
795 for (const k of Array.from(this._filterCache.keys())) {
796 if (!keys.has(k)) {
797 this._filterCache.delete(k);
798 }
799 }
800 if (this._sort && !keys.has(this._sort.key)) {
801 this._sort = null;
802 }
803 this._schedulePaint();
804 }
805 /** Read or replace the current filter map. */
806 get filters() {
807 return { ...this._filters };
808 }
809 set filters(next) {
810 this._filters = next ? { ...next } : {};
811 this._schedulePaint();
812 }
813 /** Read or set the active sort. `null` clears it. */
814 get sort() {
815 return this._sort ? { ...this._sort } : null;
816 }
817 set sort(next) {
818 this._sort = next ? { ...next } : null;
819 this._schedulePaint();
820 }
821 /** Read or replace the selection (set of row ids). */
822 get selection() {
823 return new Set(this._selection);
824 }
825 set selection(next) {
826 this._selection = new Set(next ?? []);
827 this._schedulePaint();
828 }
829 /** The currently-selected rows (resolved from `selection` + `data`). */
830 get selectedRows() {
831 const out = [];
832 this._data.forEach((row, i) => {
833 if (this._selection.has(this._getRowId(row, i))) {
834 out.push(row);
835 }
836 });
837 return out;
838 }
839 /** Stable row-id extractor. Default is row index. */
840 get getRowId() {
841 return this._getRowId;
842 }
843 set getRowId(fn) {
844 this._getRowId = typeof fn === "function" ? fn : (_r, i) => i;
845 this._schedulePaint();
846 }
847 /**
848 * Sub-table accessor. Return `null` (or omit) for rows with no
849 * children. Return `{ columns, data }` to render a nested
850 * `<wpd-table>` inline; or return any `Node` / `html\`\`` template
851 * for fully custom expanded content.
852 */
853 get subTable() {
854 return this._subTable;
855 }
856 set subTable(fn) {
857 this._subTable = typeof fn === "function" ? fn : null;
858 this._expanded.clear();
859 this._schedulePaint();
860 }
861 /** Read or replace the expansion set (row indices that are open). */
862 get expanded() {
863 return new Set(this._expanded);
864 }
865 set expanded(next) {
866 this._expanded = new Set(next ?? []);
867 this._schedulePaint();
868 }
869 // ------------------------------------------------------------------
870 // Programmatic methods
871 // ------------------------------------------------------------------
872 /** Open a row's sub-table by index. No-op if the index is out of range. */
873 expand(index) {
874 if (index < 0 || index >= this._data.length) {
875 return;
876 }
877 if (this._expanded.has(index)) {
878 return;
879 }
880 this._expanded.add(index);
881 this.emit("wpd-table-expand-change", {
882 row: this._data[index],
883 index,
884 expanded: true
885 });
886 this._schedulePaint();
887 }
888 /** Close a row's sub-table by index. No-op if it wasn't open. */
889 collapse(index) {
890 if (!this._expanded.has(index)) {
891 return;
892 }
893 this._expanded.delete(index);
894 this.emit("wpd-table-expand-change", {
895 row: this._data[index],
896 index,
897 expanded: false
898 });
899 this._schedulePaint();
900 }
901 /** Open every row that has children. */
902 expandAll() {
903 if (!this._subTable) {
904 return;
905 }
906 let changed = false;
907 for (let i = 0; i < this._data.length; i++) {
908 if (!this._subTable(this._data[i], i)) {
909 continue;
910 }
911 if (!this._expanded.has(i)) {
912 this._expanded.add(i);
913 changed = true;
914 }
915 }
916 if (changed) {
917 this._schedulePaint();
918 }
919 }
920 /** Close every open row. */
921 collapseAll() {
922 if (this._expanded.size === 0) {
923 return;
924 }
925 this._expanded.clear();
926 this._schedulePaint();
927 }
928 isExpanded(index) {
929 return this._expanded.has(index);
930 }
931 /** Drop every active filter and emit `wpd-table-filter-change`. */
932 clearFilters() {
933 if (Object.keys(this._filters).length === 0) {
934 return;
935 }
936 this._filters = {};
937 this.emit("wpd-table-filter-change", { filters: {} });
938 this._schedulePaint();
939 }
940 /** Drop the active sort and emit `wpd-table-sort-change`. */
941 clearSort() {
942 if (this._sort === null) {
943 return;
944 }
945 this._sort = null;
946 this.emit("wpd-table-sort-change", { sort: null });
947 this._schedulePaint();
948 }
949 /**
950 * Add a row id to the selection. Emits `wpd-table-selection-change`.
951 *
952 * Selection mutators (`select` / `deselect` / `selectAll` /
953 * `clearSelection`) update the affected row in place via
954 * {@link _syncSelectionDom} rather than re-rendering the whole
955 * tbody — a rebuild would tear down the focused checkbox and
956 * (because scroll-anchoring abandons a momentarily empty container)
957 * could snap scroll back to the top.
958 */
959 select(id) {
960 if (this._selection.has(id)) {
961 return;
962 }
963 const mode = this._readSelectable();
964 const previouslySelected = mode === "single" ? Array.from(this._selection) : [];
965 if (mode === "single") {
966 this._selection.clear();
967 }
968 this._selection.add(id);
969 this._emitSelectionChange();
970 this._syncSelectionDom([id, ...previouslySelected]);
971 }
972 /** Remove a row id from the selection. */
973 deselect(id) {
974 if (!this._selection.delete(id)) {
975 return;
976 }
977 this._emitSelectionChange();
978 this._syncSelectionDom([id]);
979 }
980 /** Select every row currently in `data` (multi-mode only). */
981 selectAll() {
982 if (this._readSelectable() !== "multi") {
983 return;
984 }
985 this._data.forEach(
986 (row, i) => this._selection.add(this._getRowId(row, i))
987 );
988 this._emitSelectionChange();
989 this._syncSelectionDom("all");
990 }
991 /** Empty the selection. */
992 clearSelection() {
993 if (this._selection.size === 0) {
994 return;
995 }
996 this._selection.clear();
997 this._emitSelectionChange();
998 this._syncSelectionDom("all");
999 }
1000 /**
1001 * Apply a selection change to the existing tbody DOM without
1002 * rebuilding it. Updates each affected row's `is-selected` class
1003 * and `select-row-checkbox` `checked` state, then re-syncs the
1004 * header select-all checkbox (checked / indeterminate / empty).
1005 *
1006 * @param ids `'all'` to walk every row, or an iterable of row ids
1007 * whose rows need updating. Unknown ids are silently
1008 * skipped (row may not be in the current filter/page).
1009 */
1010 _syncSelectionDom(ids) {
1011 const root = this.shadowRoot;
1012 if (!root) {
1013 return;
1014 }
1015 const tbody = root.querySelector("tbody");
1016 if (!tbody) {
1017 return;
1018 }
1019 let needle = null;
1020 if (ids !== "all") {
1021 needle = /* @__PURE__ */ new Set();
1022 for (const id of ids) {
1023 needle.add(String(id));
1024 }
1025 }
1026 const rows = tbody.querySelectorAll(
1027 "tr[data-row-id]"
1028 );
1029 for (const tr of rows) {
1030 const rowIdStr = tr.dataset.rowId;
1031 if (rowIdStr === void 0) {
1032 continue;
1033 }
1034 if (needle && !needle.has(rowIdStr)) {
1035 continue;
1036 }
1037 const idx = Number(tr.dataset.rowIndex);
1038 if (!Number.isFinite(idx)) {
1039 continue;
1040 }
1041 const row = this._data[idx];
1042 if (row === void 0) {
1043 continue;
1044 }
1045 const id = this._getRowId(row, idx);
1046 const isSelected = this._selection.has(id);
1047 tr.classList.toggle("is-selected", isSelected);
1048 const cb = tr.querySelector(
1049 "input.select-row-checkbox"
1050 );
1051 if (cb && cb.checked !== isSelected) {
1052 cb.checked = isSelected;
1053 }
1054 }
1055 const headerCb = root.querySelector(
1056 "thead .select-all-checkbox"
1057 );
1058 if (headerCb) {
1059 const total = this._data.length;
1060 const selectedCount = this._countSelectedInData();
1061 headerCb.checked = total > 0 && selectedCount === total;
1062 headerCb.indeterminate = selectedCount > 0 && selectedCount < total;
1063 }
1064 }
1065 /** Scroll the (filtered) row at `index` into view inside the table's scroll container. */
1066 scrollToRow(index) {
1067 const root = this.shadowRoot;
1068 if (!root) {
1069 return;
1070 }
1071 const rows = root.querySelectorAll(
1072 "tbody tr:not(.subtable):not(.empty):not(.skeleton)"
1073 );
1074 const row = rows[index];
1075 if (row) {
1076 row.scrollIntoView({ block: "nearest", inline: "nearest" });
1077 }
1078 }
1079 connectedCallback() {
1080 super.connectedCallback();
1081 this._schedulePaint();
1082 }
1083 disconnectedCallback() {
1084 this._resizeObserver?.disconnect();
1085 this._resizeObserver = null;
1086 if (this._stickyRafHandle !== null && typeof cancelAnimationFrame !== "undefined") {
1087 cancelAnimationFrame(this._stickyRafHandle);
1088 this._stickyRafHandle = null;
1089 }
1090 }
1091 /**
1092 * Force a sticky-offsets recompute. Public escape hatch for the
1093 * rare case where layout settles after every internal hook has
1094 * fired — e.g. an out-of-band font swap or a JS-driven width
1095 * change on an ancestor that doesn't bubble through ResizeObserver.
1096 *
1097 * Usually you don't need this: the component schedules recomputes
1098 * on a microtask + animation frame after every paint, and a
1099 * ResizeObserver on the inner scroll element catches geometry
1100 * changes thereafter. Reach for `recomputeLayout()` only if you've
1101 * confirmed that all of those pathways missed your case.
1102 */
1103 recomputeLayout() {
1104 this._applyStickyOffsets();
1105 this._measureHeaderHeight();
1106 }
1107 // ------------------------------------------------------------------
1108 // Skeleton + paint pipeline
1109 // ------------------------------------------------------------------
1110 render() {
1111 return html`
1112 <div class="scroll" part="scroll">
1113 <table part="table">
1114 <colgroup></colgroup>
1115 <thead></thead>
1116 <tbody></tbody>
1117 </table>
1118 </div>
1119 `;
1120 }
1121 requestUpdate() {
1122 super.requestUpdate();
1123 this._schedulePaint();
1124 }
1125 _schedulePaint() {
1126 if (this._paintScheduled || !this.isConnected) {
1127 return;
1128 }
1129 this._paintScheduled = true;
1130 queueMicrotask(() => {
1131 this._paintScheduled = false;
1132 if (!this.isConnected) {
1133 return;
1134 }
1135 this._paint();
1136 });
1137 }
1138 _paint() {
1139 const root = this.shadowRoot;
1140 if (!root) {
1141 return;
1142 }
1143 if (!root.querySelector("tbody")) {
1144 render(this.render(), root);
1145 }
1146 const colgroup = root.querySelector("colgroup");
1147 const thead = root.querySelector("thead");
1148 const tbody = root.querySelector("tbody");
1149 if (!colgroup || !thead || !tbody) {
1150 return;
1151 }
1152 const cols = this._effectiveColumns();
1153 const stickyN = this._readStickyColumns();
1154 this._lastStickyIndex = this._computeLastStickyIndex(cols, stickyN);
1155 this._paintColgroup(colgroup, cols);
1156 this._paintHead(thead, cols, stickyN);
1157 this._paintBody(tbody, cols, stickyN);
1158 this._applyStickyOffsets();
1159 this._measureHeaderHeight();
1160 this._scheduleStickyOffsets();
1161 this._maybeWarnStickyHeader();
1162 this._maybeWarnLoadingDesync(tbody);
1163 this._ensureResizeObserver();
1164 }
1165 /**
1166 * Diagnostic for the "I set `loading` but the skeleton never
1167 * appeared" footgun. If we get here with the attribute on but no
1168 * `.skeleton` rows in `tbody`, something between attribute set and
1169 * paint went off the rails — historically this happened when the
1170 * base `Component.attributeChangedCallback` called `_scheduleRender`
1171 * directly, bypassing our `requestUpdate` override. Same pattern as
1172 * the sticky-columns 0px tripwire: should never fire, but if it
1173 * does, names the bug instead of leaving the dev guessing.
1174 */
1175 _maybeWarnLoadingDesync(tbody) {
1176 if (this._loadingDesyncWarned) {
1177 return;
1178 }
1179 if (!this.hasAttribute("loading")) {
1180 return;
1181 }
1182 if (tbody.querySelector("tr.skeleton")) {
1183 return;
1184 }
1185 this._loadingDesyncWarned = true;
1186 console.warn(
1187 "[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."
1188 );
1189 }
1190 /**
1191 * Belt-and-braces sticky-offset scheduling.
1192 *
1193 * - Microtask: cheap, fires after the current task drains. Fixes
1194 * mounts where the synchronous read in `_paint` happened before
1195 * a sibling style applied.
1196 * - rAF: fires before the next paint. Catches "layout settles
1197 * after a queued style mutation" races — the most common cause
1198 * of "col 1 ended up at inset-inline-start: 0px".
1199 *
1200 * Both reduce to a no-op when nothing changed. The cost is two
1201 * extra DOM reads per paint; the win is the bug class disappears.
1202 */
1203 _scheduleStickyOffsets() {
1204 if (!this._stickyMicroScheduled) {
1205 this._stickyMicroScheduled = true;
1206 queueMicrotask(() => {
1207 this._stickyMicroScheduled = false;
1208 if (this.isConnected) {
1209 this._applyStickyOffsets();
1210 }
1211 });
1212 }
1213 if (this._stickyRafHandle === null && typeof requestAnimationFrame !== "undefined") {
1214 this._stickyRafHandle = requestAnimationFrame(() => {
1215 this._stickyRafHandle = null;
1216 if (this.isConnected) {
1217 this._applyStickyOffsets();
1218 this._measureHeaderHeight();
1219 }
1220 });
1221 }
1222 }
1223 /**
1224 * Wire a `ResizeObserver` on the inner `.scroll` element (NOT the
1225 * host). Why: the host's outer width is often pinned by its parent
1226 * panel — a vertical scrollbar appearing inside the table changes
1227 * the inner scroll-area width by ~15px without changing the host
1228 * size. Observing the host would miss that reflow and leave sticky
1229 * offsets stale.
1230 *
1231 * Idempotent — runs once after the first paint produces a real
1232 * `.scroll` element. Disconnect happens in `disconnectedCallback`.
1233 */
1234 _ensureResizeObserver() {
1235 if (this._resizeObserver) {
1236 return;
1237 }
1238 if (typeof ResizeObserver === "undefined") {
1239 return;
1240 }
1241 const scroll = this.shadowRoot?.querySelector(
1242 ".scroll"
1243 );
1244 if (!scroll) {
1245 return;
1246 }
1247 this._resizeObserver = new ResizeObserver(() => {
1248 if (!this.isConnected) {
1249 return;
1250 }
1251 this._applyStickyOffsets();
1252 this._measureHeaderHeight();
1253 this._stickyHeaderWarned = false;
1254 this._maybeWarnStickyHeader();
1255 });
1256 this._resizeObserver.observe(scroll);
1257 this._resizeObserver.observe(this);
1258 }
1259 _paintColgroup(colgroup, cols) {
1260 const out = [];
1261 for (const c of cols) {
1262 const col = document.createElement("col");
1263 if (c.width) {
1264 col.style.width = c.width;
1265 }
1266 out.push(col);
1267 }
1268 colgroup.replaceChildren(...out);
1269 }
1270 _paintHead(thead, cols, stickyN) {
1271 const newHeaderRow = document.createElement("tr");
1272 newHeaderRow.setAttribute("part", "header-row");
1273 for (let i = 0; i < cols.length; i++) {
1274 newHeaderRow.appendChild(this._buildHeaderCell(cols[i], i, stickyN));
1275 }
1276 const existingHeader = thead.querySelector(
1277 ':scope > tr[part="header-row"]'
1278 );
1279 if (existingHeader) {
1280 thead.replaceChild(newHeaderRow, existingHeader);
1281 } else {
1282 thead.insertBefore(newHeaderRow, thead.firstChild);
1283 }
1284 const hasFilter = cols.some(
1285 (c) => c.filter || Array.isArray(c.filterOptions) || typeof c.filterRender === "function"
1286 );
1287 let existingFilter = thead.querySelector(
1288 ":scope > tr.filter-row"
1289 );
1290 if (hasFilter) {
1291 const cells = [];
1292 for (let i = 0; i < cols.length; i++) {
1293 cells.push(this._buildFilterCell(cols[i], i, stickyN));
1294 }
1295 if (!existingFilter) {
1296 existingFilter = document.createElement("tr");
1297 existingFilter.classList.add("filter-row");
1298 existingFilter.setAttribute("part", "filter-row");
1299 thead.appendChild(existingFilter);
1300 }
1301 const current = Array.from(existingFilter.children);
1302 let same = current.length === cells.length;
1303 if (same) {
1304 for (let i = 0; i < cells.length; i++) {
1305 if (current[i] !== cells[i]) {
1306 same = false;
1307 break;
1308 }
1309 }
1310 }
1311 if (!same) {
1312 const wanted = new Set(cells);
1313 for (const cell of cells) {
1314 existingFilter.appendChild(cell);
1315 }
1316 for (const child of Array.from(existingFilter.children)) {
1317 if (!wanted.has(child)) {
1318 existingFilter.removeChild(child);
1319 }
1320 }
1321 }
1322 } else if (existingFilter) {
1323 existingFilter.remove();
1324 }
1325 }
1326 _buildHeaderCell(col, index, stickyN) {
1327 const th = document.createElement("th");
1328 th.setAttribute("scope", "col");
1329 th.dataset.key = col.key;
1330 this._applyCellClasses(th, col, index, stickyN);
1331 if (col.minWidth) {
1332 th.style.minWidth = col.minWidth;
1333 }
1334 if (col.key === SELECT_KEY) {
1335 const mode = this._readSelectable();
1336 if (mode === "multi") {
1337 const cb = document.createElement("input");
1338 cb.type = "checkbox";
1339 cb.className = "select-all-checkbox";
1340 cb.setAttribute("data-noclick", "");
1341 cb.setAttribute("aria-label", "Select all rows");
1342 const total = this._data.length;
1343 const selectedCount = this._countSelectedInData();
1344 cb.checked = total > 0 && selectedCount === total;
1345 cb.indeterminate = selectedCount > 0 && selectedCount < total;
1346 cb.addEventListener("change", () => {
1347 if (cb.checked) {
1348 this.selectAll();
1349 } else {
1350 this.clearSelection();
1351 }
1352 });
1353 th.appendChild(cb);
1354 }
1355 return th;
1356 }
1357 th.textContent = col.label ?? (col.key === EXPANDER_KEY ? "" : col.key);
1358 if (col.sortable) {
1359 th.classList.add("is-sortable");
1360 const isActive = this._sort?.key === col.key;
1361 const indicator = document.createElement("span");
1362 indicator.className = "sort-indicator";
1363 let arrow = "";
1364 if (isActive) {
1365 arrow = this._sort.direction === "asc" ? "" : "";
1366 }
1367 indicator.textContent = arrow;
1368 th.appendChild(indicator);
1369 if (isActive) {
1370 th.classList.add(
1371 this._sort.direction === "asc" ? "sort-asc" : "sort-desc"
1372 );
1373 }
1374 th.addEventListener("click", () => this._cycleSort(col.key));
1375 }
1376 return th;
1377 }
1378 _buildFilterCell(col, index, stickyN) {
1379 const cached = this._filterCache.get(col.key);
1380 const hasExplicitOptions = Array.isArray(col.filterOptions);
1381 const hasCustomRender = typeof col.filterRender === "function";
1382 let desiredKind;
1383 if (!col.filter && !hasExplicitOptions && !hasCustomRender || col.key === EXPANDER_KEY || col.key === SELECT_KEY) {
1384 desiredKind = "none";
1385 } else if (hasCustomRender) {
1386 desiredKind = "custom";
1387 } else if (col.filter === "select" || hasExplicitOptions) {
1388 desiredKind = "select";
1389 } else {
1390 desiredKind = "text";
1391 }
1392 if (cached && cached.kind === desiredKind) {
1393 cached.th.className = "";
1394 this._applyCellClasses(cached.th, col, index, stickyN);
1395 if (desiredKind === "select") {
1396 const select = cached.control;
1397 const opts = this._resolveFilterOptions(col);
1398 const optsKey = opts.map((o) => o.value).join("|");
1399 if (optsKey !== cached.optionsKey) {
1400 this._populateSelect(select, opts, this._filters[col.key] ?? "");
1401 cached.optionsKey = optsKey;
1402 } else {
1403 select.value = this._filters[col.key] ?? "";
1404 }
1405 } else if (desiredKind === "text") {
1406 const input = cached.control;
1407 const want = this._filters[col.key] ?? "";
1408 if (input.value !== want && input.ownerDocument.activeElement !== input) {
1409 input.value = want;
1410 }
1411 } else if (desiredKind === "custom" && col.filterRender) {
1412 col.filterRender(cached.th, {
1413 value: this._filters[col.key] ?? "",
1414 setValue: (next) => this._onFilterChange(col.key, next),
1415 col
1416 });
1417 }
1418 return cached.th;
1419 }
1420 const th = document.createElement("th");
1421 this._applyCellClasses(th, col, index, stickyN);
1422 if (desiredKind === "none") {
1423 this._filterCache.set(col.key, {
1424 th,
1425 control: null,
1426 optionsKey: "",
1427 kind: "none"
1428 });
1429 return th;
1430 }
1431 if (desiredKind === "custom" && col.filterRender) {
1432 col.filterRender(th, {
1433 value: this._filters[col.key] ?? "",
1434 setValue: (next) => this._onFilterChange(col.key, next),
1435 col
1436 });
1437 this._filterCache.set(col.key, {
1438 th,
1439 control: null,
1440 optionsKey: "",
1441 kind: "custom"
1442 });
1443 return th;
1444 }
1445 let control;
1446 let optionsKey = "";
1447 if (desiredKind === "select") {
1448 const select = document.createElement("select");
1449 select.classList.add("filter-select");
1450 select.setAttribute("data-noclick", "");
1451 select.setAttribute(
1452 "aria-label",
1453 `Filter ${col.label ?? col.key}`
1454 );
1455 const opts = this._resolveFilterOptions(col);
1456 this._populateSelect(select, opts, this._filters[col.key] ?? "");
1457 optionsKey = opts.map((o) => o.value).join("|");
1458 select.addEventListener("change", () => {
1459 this._onFilterChange(col.key, select.value);
1460 });
1461 control = select;
1462 } else {
1463 const input = document.createElement("input");
1464 input.type = "search";
1465 input.classList.add("filter-input");
1466 input.setAttribute("data-noclick", "");
1467 input.setAttribute("placeholder", "Filter…");
1468 input.setAttribute("aria-label", `Filter ${col.label ?? col.key}`);
1469 input.value = this._filters[col.key] ?? "";
1470 input.addEventListener("input", () => {
1471 this._onFilterChange(col.key, input.value);
1472 });
1473 control = input;
1474 }
1475 th.appendChild(control);
1476 this._filterCache.set(col.key, {
1477 th,
1478 control,
1479 optionsKey,
1480 kind: desiredKind
1481 });
1482 return th;
1483 }
1484 _populateSelect(select, options, current) {
1485 select.replaceChildren();
1486 const all = document.createElement("option");
1487 all.value = "";
1488 all.textContent = "All";
1489 select.appendChild(all);
1490 for (const opt of options) {
1491 const el = document.createElement("option");
1492 el.value = opt.value;
1493 el.textContent = opt.label;
1494 if (opt.value === current) {
1495 el.selected = true;
1496 }
1497 select.appendChild(el);
1498 }
1499 select.value = current;
1500 }
1501 /**
1502 * Resolve the option list for a select-filter column. Explicit
1503 * `filterOptions` win — that's the contract for server-driven
1504 * tables that need the dropdown to list values not present on
1505 * the current page. Without `filterOptions`, fall back to the
1506 * unique row values in the column (legacy behaviour for
1507 * client-side tables).
1508 */
1509 _resolveFilterOptions(col) {
1510 if (Array.isArray(col.filterOptions)) {
1511 return col.filterOptions;
1512 }
1513 return this._uniqueValues(col.key).map((v) => ({
1514 value: v,
1515 label: v
1516 }));
1517 }
1518 // ------------------------------------------------------------------
1519 // Body
1520 // ------------------------------------------------------------------
1521 _paintBody(tbody, cols, stickyN) {
1522 tbody.replaceChildren();
1523 if (this.hasAttribute("loading")) {
1524 const count = this._readLoadingRows();
1525 for (let i = 0; i < count; i++) {
1526 tbody.appendChild(this._buildSkeletonRow(cols, i));
1527 }
1528 return;
1529 }
1530 const filtered = this._sortedRows(this._filteredRows());
1531 if (filtered.length === 0) {
1532 tbody.appendChild(this._buildEmptyRow(cols.length));
1533 return;
1534 }
1535 for (const { row, index } of filtered) {
1536 tbody.appendChild(this._buildBodyRow(row, index, cols, stickyN));
1537 if (this._expanded.has(index) && this._subTable) {
1538 const sub = this._subTable(row, index);
1539 if (sub) {
1540 tbody.appendChild(this._buildSubTableRow(sub, cols.length));
1541 }
1542 }
1543 }
1544 }
1545 _buildEmptyRow(colspan) {
1546 const tr = document.createElement("tr");
1547 tr.classList.add("empty");
1548 const td = document.createElement("td");
1549 td.colSpan = colspan;
1550 const slot = document.createElement("slot");
1551 slot.name = "empty";
1552 slot.textContent = this.getAttribute("empty") || "No data";
1553 td.appendChild(slot);
1554 tr.appendChild(td);
1555 return tr;
1556 }
1557 _buildSkeletonRow(cols, seed) {
1558 const tr = document.createElement("tr");
1559 tr.classList.add("skeleton");
1560 tr.setAttribute("aria-hidden", "true");
1561 for (const _c of cols) {
1562 const td = document.createElement("td");
1563 const bar = document.createElement("span");
1564 bar.className = "skeleton-bar";
1565 const widthPct = 50 + (seed * 7 + tr.children.length * 13) % 40;
1566 bar.style.width = `${widthPct}%`;
1567 td.appendChild(bar);
1568 tr.appendChild(td);
1569 }
1570 return tr;
1571 }
1572 _buildBodyRow(row, rowIndex, cols, stickyN) {
1573 const tr = document.createElement("tr");
1574 tr.setAttribute("part", "row");
1575 tr.dataset.rowIndex = String(rowIndex);
1576 const id = this._getRowId(row, rowIndex);
1577 tr.dataset.rowId = String(id);
1578 if (this._selection.has(id)) {
1579 tr.classList.add("is-selected");
1580 }
1581 tr.addEventListener("click", (e) => {
1582 this._onRowClick(row, rowIndex, e);
1583 });
1584 for (let i = 0; i < cols.length; i++) {
1585 tr.appendChild(
1586 this._buildBodyCell(cols[i], i, row, rowIndex, stickyN)
1587 );
1588 }
1589 return tr;
1590 }
1591 _buildBodyCell(col, colIndex, row, rowIndex, stickyN) {
1592 const td = document.createElement("td");
1593 this._applyCellClasses(td, col, colIndex, stickyN);
1594 if (col.minWidth) {
1595 td.style.minWidth = col.minWidth;
1596 }
1597 if (col.key === SELECT_KEY) {
1598 const id = this._getRowId(row, rowIndex);
1599 const cb = document.createElement("input");
1600 cb.type = "checkbox";
1601 cb.className = "select-row-checkbox";
1602 cb.setAttribute("data-noclick", "");
1603 cb.setAttribute("aria-label", "Select row");
1604 cb.checked = this._selection.has(id);
1605 cb.addEventListener("change", () => {
1606 if (cb.checked) {
1607 this.select(id);
1608 } else {
1609 this.deselect(id);
1610 }
1611 });
1612 td.appendChild(cb);
1613 return td;
1614 }
1615 if (col.key === EXPANDER_KEY) {
1616 const hasChildren = this._subTable ? !!this._subTable(row, rowIndex) : false;
1617 if (!hasChildren) {
1618 return td;
1619 }
1620 const isOpen = this._expanded.has(rowIndex);
1621 const btn = document.createElement("button");
1622 btn.type = "button";
1623 btn.className = "expander";
1624 btn.setAttribute("data-noclick", "");
1625 btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
1626 btn.setAttribute(
1627 "aria-label",
1628 isOpen ? "Collapse row" : "Expand row"
1629 );
1630 btn.textContent = isOpen ? "" : "";
1631 btn.addEventListener("click", (e) => {
1632 this._toggleRow(rowIndex, row, e);
1633 });
1634 td.appendChild(btn);
1635 return td;
1636 }
1637 const value = row[col.key];
1638 if (col.render) {
1639 const out = col.render(value, row, rowIndex);
1640 this._mountCellContent(td, out);
1641 } else if (value !== null && value !== void 0) {
1642 td.textContent = String(value);
1643 }
1644 return td;
1645 }
1646 _buildSubTableRow(sub, colspan) {
1647 const tr = document.createElement("tr");
1648 tr.classList.add("subtable");
1649 tr.setAttribute("part", "subtable-row");
1650 const td = document.createElement("td");
1651 td.colSpan = colspan;
1652 const inner = document.createElement("div");
1653 inner.classList.add("subtable-inner");
1654 if (sub instanceof Node) {
1655 inner.appendChild(sub);
1656 } else if (isTemplateResult(sub)) {
1657 render(sub, inner);
1658 } else {
1659 const nested = document.createElement("wpd-table");
1660 nested.columns = sub.columns;
1661 nested.data = sub.data;
1662 if (sub.subTable) {
1663 nested.subTable = sub.subTable;
1664 }
1665 inner.appendChild(nested);
1666 }
1667 td.appendChild(inner);
1668 tr.appendChild(td);
1669 return tr;
1670 }
1671 _mountCellContent(td, out) {
1672 if (typeof out === "string") {
1673 td.textContent = out;
1674 return;
1675 }
1676 if (out instanceof Node) {
1677 td.appendChild(out);
1678 return;
1679 }
1680 if (isTemplateResult(out)) {
1681 render(out, td);
1682 }
1683 }
1684 // ------------------------------------------------------------------
1685 // Behavior
1686 // ------------------------------------------------------------------
1687 _onFilterChange(key, value) {
1688 if (value === "") {
1689 delete this._filters[key];
1690 } else {
1691 this._filters[key] = value;
1692 }
1693 this.emit("wpd-table-filter-change", { filters: { ...this._filters } });
1694 const root = this.shadowRoot;
1695 const tbody = root?.querySelector("tbody");
1696 if (tbody) {
1697 const cols = this._effectiveColumns();
1698 const stickyN = this._readStickyColumns();
1699 this._lastStickyIndex = this._computeLastStickyIndex(cols, stickyN);
1700 this._paintBody(tbody, cols, stickyN);
1701 this._applyStickyOffsets();
1702 }
1703 }
1704 _onRowClick(row, index, e) {
1705 const path = e.composedPath?.() ?? [];
1706 for (const node of path) {
1707 if (node instanceof Element && node.hasAttribute("data-noclick")) {
1708 return;
1709 }
1710 if (node === this) {
1711 break;
1712 }
1713 }
1714 this.emit("wpd-table-row-click", { row, index, originalEvent: e });
1715 }
1716 _toggleRow(index, row, e) {
1717 e.stopPropagation();
1718 const isOpen = this._expanded.has(index);
1719 if (isOpen) {
1720 this._expanded.delete(index);
1721 } else {
1722 this._expanded.add(index);
1723 }
1724 this.emit("wpd-table-expand-change", {
1725 row,
1726 index,
1727 expanded: !isOpen
1728 });
1729 this._schedulePaint();
1730 }
1731 _cycleSort(key) {
1732 if (!this._sort || this._sort.key !== key) {
1733 this._sort = { key, direction: "asc" };
1734 } else if (this._sort.direction === "asc") {
1735 this._sort = { key, direction: "desc" };
1736 } else {
1737 this._sort = null;
1738 }
1739 this.emit("wpd-table-sort-change", {
1740 sort: this._sort ? { ...this._sort } : null
1741 });
1742 this._schedulePaint();
1743 }
1744 _emitSelectionChange() {
1745 this.emit("wpd-table-selection-change", {
1746 selection: Array.from(this._selection),
1747 rows: this.selectedRows
1748 });
1749 }
1750 // ------------------------------------------------------------------
1751 // Filtering + sorting
1752 // ------------------------------------------------------------------
1753 _filteredRows() {
1754 const out = [];
1755 const active = Object.keys(this._filters).filter(
1756 (k) => this._filters[k] !== ""
1757 );
1758 for (let i = 0; i < this._data.length; i++) {
1759 const row = this._data[i];
1760 let pass = true;
1761 for (const key of active) {
1762 const col = this._columns.find((c) => c.key === key);
1763 if (col && typeof col.filterRender === "function") {
1764 continue;
1765 }
1766 const filter = this._filters[key] ?? "";
1767 const cell = row[key];
1768 const cellStr = cell === null || cell === void 0 ? "" : String(cell);
1769 if (col?.filter === "select") {
1770 if (cellStr !== filter) {
1771 pass = false;
1772 break;
1773 }
1774 } else if (!cellStr.toLowerCase().includes(filter.toLowerCase())) {
1775 pass = false;
1776 break;
1777 }
1778 }
1779 if (pass) {
1780 out.push({ row, index: i });
1781 }
1782 }
1783 return out;
1784 }
1785 _sortedRows(rows) {
1786 if (!this._sort) {
1787 return rows;
1788 }
1789 const col = this._columns.find((c) => c.key === this._sort.key);
1790 if (!col) {
1791 return rows;
1792 }
1793 const dir = this._sort.direction === "desc" ? -1 : 1;
1794 const out = rows.slice();
1795 out.sort((a, b) => {
1796 const av = col.sortValue ? col.sortValue(a.row, a.row[col.key]) : a.row[col.key];
1797 const bv = col.sortValue ? col.sortValue(b.row, b.row[col.key]) : b.row[col.key];
1798 return compareValues(av, bv) * dir;
1799 });
1800 return out;
1801 }
1802 _uniqueValues(key) {
1803 const seen = /* @__PURE__ */ new Set();
1804 for (const row of this._data) {
1805 const v = row[key];
1806 if (v === null || v === void 0) {
1807 continue;
1808 }
1809 seen.add(String(v));
1810 }
1811 return Array.from(seen).sort();
1812 }
1813 _countSelectedInData() {
1814 let n = 0;
1815 this._data.forEach((row, i) => {
1816 if (this._selection.has(this._getRowId(row, i))) {
1817 n++;
1818 }
1819 });
1820 return n;
1821 }
1822 // ------------------------------------------------------------------
1823 // Sticky columns + attribute reads
1824 // ------------------------------------------------------------------
1825 _readStickyColumns() {
1826 const raw = parseInt(this.getAttribute("sticky-columns") || "0", 10);
1827 return Number.isFinite(raw) && raw > 0 ? raw : 0;
1828 }
1829 _readLoadingRows() {
1830 const raw = parseInt(this.getAttribute("loading-rows") || "5", 10);
1831 return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 100) : 5;
1832 }
1833 _readSelectable() {
1834 const v = this.getAttribute("selectable");
1835 if (v === "single") {
1836 return "single";
1837 }
1838 if (v === "multi" || v === "") {
1839 return "multi";
1840 }
1841 return null;
1842 }
1843 /**
1844 * Sticky-band membership. The first N columns get pinned, with two
1845 * per-column overrides: `column.sticky = true` opts in even outside
1846 * the band; `column.sticky = false` opts out within it.
1847 */
1848 _isStickyIndex(index, stickyN, col) {
1849 if (col.sticky === false) {
1850 return false;
1851 }
1852 if (col.sticky === true) {
1853 return true;
1854 }
1855 return index < stickyN;
1856 }
1857 _computeLastStickyIndex(cols, stickyN) {
1858 let last = -1;
1859 for (let i = 0; i < cols.length; i++) {
1860 if (this._isStickyIndex(i, stickyN, cols[i])) {
1861 last = i;
1862 }
1863 }
1864 return last;
1865 }
1866 _applyCellClasses(cell, col, index, stickyN) {
1867 if (col.key === EXPANDER_KEY) {
1868 cell.classList.add("col-expander");
1869 }
1870 if (col.key === SELECT_KEY) {
1871 cell.classList.add("col-select");
1872 }
1873 if (col.align === "center") {
1874 cell.classList.add("align-center");
1875 }
1876 if (col.align === "end") {
1877 cell.classList.add("align-end");
1878 }
1879 const sticky = this._isStickyIndex(index, stickyN, col);
1880 if (sticky) {
1881 cell.classList.add("is-sticky");
1882 if (index === this._lastStickyIndex) {
1883 cell.classList.add("is-sticky-edge");
1884 }
1885 }
1886 }
1887 _effectiveColumns() {
1888 const out = [];
1889 if (this._readSelectable()) {
1890 out.push({
1891 key: SELECT_KEY,
1892 label: "",
1893 // The descriptor width is painted onto a `<col>`
1894 // element and is the authoritative column-width
1895 // source in table-layout: auto — CSS `td { width }`
1896 // is ignored once `<col>` has a value. Pair with
1897 // the matching `td.col-select` rule (zero
1898 // `padding-inline`, `text-align: center`) so the
1899 // checkbox sits with breathing room on both sides.
1900 width: "40px",
1901 align: "center"
1902 });
1903 }
1904 if (this._subTable) {
1905 out.push({
1906 key: EXPANDER_KEY,
1907 label: "",
1908 // Same contract as col-select. 36px column +
1909 // 20px button + zero padding centers the chevron
1910 // with ~8px on each side.
1911 width: "36px",
1912 align: "center"
1913 });
1914 }
1915 out.push(...this._columns);
1916 return out;
1917 }
1918 /**
1919 * Walk the header row, sum the natural widths of the sticky cells,
1920 * then write cumulative `inset-inline-start` offsets onto every
1921 * row's matching cells.
1922 */
1923 _applyStickyOffsets() {
1924 const root = this.shadowRoot;
1925 if (!root) {
1926 return;
1927 }
1928 const headRow = root.querySelector("thead tr");
1929 if (!headRow) {
1930 return;
1931 }
1932 const ths = Array.from(headRow.children);
1933 const offsets = [];
1934 let acc = 0;
1935 for (let i = 0; i < ths.length; i++) {
1936 offsets[i] = acc;
1937 if (ths[i].classList.contains("is-sticky")) {
1938 acc += ths[i].offsetWidth;
1939 }
1940 }
1941 const rows = root.querySelectorAll(
1942 "thead tr, tbody tr:not(.subtable):not(.empty):not(.skeleton)"
1943 );
1944 rows.forEach((r) => {
1945 const cells = Array.from(r.children);
1946 for (let i = 0; i < cells.length; i++) {
1947 if (cells[i].classList.contains("is-sticky")) {
1948 cells[i].style.insetInlineStart = `${offsets[i]}px`;
1949 }
1950 }
1951 });
1952 this._maybeWarnStickyOffsetRace(ths, offsets);
1953 }
1954 _maybeWarnStickyOffsetRace(ths, offsets) {
1955 if (this._stickyRaceWarned) {
1956 return;
1957 }
1958 const stickyN = this._readStickyColumns();
1959 if (stickyN < 2) {
1960 return;
1961 }
1962 const lastIdx = Math.min(stickyN - 1, ths.length - 1);
1963 if (lastIdx <= 0) {
1964 return;
1965 }
1966 if (offsets[lastIdx] !== 0) {
1967 return;
1968 }
1969 if (this.offsetWidth === 0) {
1970 return;
1971 }
1972 this._stickyRaceWarned = true;
1973 const w0 = ths[0]?.offsetWidth ?? 0;
1974 console.warn(
1975 `[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.`
1976 );
1977 }
1978 _measureHeaderHeight() {
1979 const root = this.shadowRoot;
1980 if (!root) {
1981 return;
1982 }
1983 const headRow = root.querySelector("thead tr");
1984 if (!headRow) {
1985 return;
1986 }
1987 const h = headRow.offsetHeight;
1988 if (h > 0) {
1989 this.style.setProperty("--wpd-table-header-height", `${h}px`);
1990 }
1991 }
1992 /**
1993 * Once-per-element warning for the most common sticky-header
1994 * mistake: forgetting to give the table a scroll container. Without
1995 * a max-height (or a scrolling ancestor), `position: sticky`
1996 * silently does nothing because there's no scrollport for it to
1997 * stick within.
1998 */
1999 _maybeWarnStickyHeader() {
2000 if (this._stickyHeaderWarned) {
2001 return;
2002 }
2003 if (!this.hasAttribute("sticky-header")) {
2004 return;
2005 }
2006 if (this.hasAttribute("loading") || this._data.length < 8) {
2007 return;
2008 }
2009 const scroll = this.shadowRoot?.querySelector(
2010 ".scroll"
2011 );
2012 if (!scroll) {
2013 return;
2014 }
2015 if (scroll.offsetWidth === 0) {
2016 return;
2017 }
2018 if (scroll.scrollHeight <= scroll.clientHeight + 1) {
2019 this._stickyHeaderWarned = true;
2020 console.warn(
2021 "[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."
2022 );
2023 }
2024 }
2025 };
2026 _WpdTable.props = [
2027 "stickyColumns",
2028 "stickyHeader",
2029 "striped",
2030 "hover",
2031 "compact",
2032 "bordered",
2033 "empty",
2034 "loading",
2035 "loadingRows",
2036 "selectable"
2037 ];
2038 _WpdTable.styles = [styles$c];
2039 _WpdTable.help = {
2040 title: "Table",
2041 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.",
2042 status: "experimental",
2043 since: "0.18.0",
2044 props: [
2045 {
2046 name: "sticky-columns",
2047 type: "integer",
2048 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."
2049 },
2050 {
2051 name: "sticky-header",
2052 type: "boolean",
2053 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."
2054 },
2055 { name: "striped", type: "boolean", description: "Zebra rows." },
2056 { name: "hover", type: "boolean", description: "Highlight rows on hover." },
2057 { name: "compact", type: "boolean", description: "Tighter padding + smaller font." },
2058 { name: "bordered", type: "boolean", description: "Vertical cell borders." },
2059 {
2060 name: "empty",
2061 type: "string",
2062 description: "Fallback text shown when there are no rows. For richer empty states, project light-DOM content into the `empty` slot."
2063 },
2064 {
2065 name: "loading",
2066 type: "boolean",
2067 description: "Paint shimmering skeleton rows in place of body content. Filters / sort headers stay live."
2068 },
2069 {
2070 name: "loading-rows",
2071 type: "integer",
2072 description: "Number of skeleton rows when loading. Default 5."
2073 },
2074 {
2075 name: "selectable",
2076 type: '"single" | "multi"',
2077 description: "Auto-prepend a checkbox column. `multi` puts a select-all checkbox in the header; `single` enforces at-most-one selected."
2078 }
2079 ],
2080 events: [
2081 { name: "wpd-table-filter-change", description: "Filter input changed." },
2082 { name: "wpd-table-sort-change", description: "Header click cycled the sort." },
2083 { name: "wpd-table-selection-change", description: "Selection set changed." },
2084 { name: "wpd-table-row-click", description: "Body row clicked (skips data-noclick descendants)." },
2085 { name: "wpd-table-expand-change", description: "Sub-table toggled." }
2086 ],
2087 slots: [
2088 { name: "empty", description: "Custom empty-state content (CTA, illustration, etc.)." }
2089 ],
2090 cssProps: [
2091 { name: "--wpd-table-bg" },
2092 { name: "--wpd-table-border" },
2093 { name: "--wpd-table-column-border" },
2094 { name: "--wpd-table-header-bg" },
2095 { name: "--wpd-table-row-hover" },
2096 { name: "--wpd-table-stripe" },
2097 { name: "--wpd-table-cell-padding" },
2098 { name: "--wpd-table-font-size" },
2099 { name: "--wpd-table-max-height" },
2100 { name: "--wpd-table-skeleton-color" }
2101 ],
2102 example: html`
2103 <wpd-table id="sample-table" sticky-header striped hover></wpd-table>
2104 `
2105 };
2106 let WpdTable = _WpdTable;
2107 function isTemplateResult(v) {
2108 return !!v && v.__wpdHtml === true;
2109 }
2110 function compareValues(a, b) {
2111 if (a === b) {
2112 return 0;
2113 }
2114 if (a === null || a === void 0) {
2115 return -1;
2116 }
2117 if (b === null || b === void 0) {
2118 return 1;
2119 }
2120 if (typeof a === "number" && typeof b === "number") {
2121 return a - b;
2122 }
2123 if (a instanceof Date && b instanceof Date) {
2124 return a.getTime() - b.getTime();
2125 }
2126 const an = Number(a);
2127 const bn = Number(b);
2128 if (Number.isFinite(an) && Number.isFinite(bn)) {
2129 return an - bn;
2130 }
2131 return String(a).localeCompare(String(b));
2132 }
2133 defineComponent("wpd-table", WpdTable);
2134 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}`;
2135 const NOCLICK_SELECTOR = "[data-noclick]";
2136 const _WpdCard = class _WpdCard extends Component {
2137 constructor() {
2138 super(...arguments);
2139 this._onClick = (ev) => {
2140 if (!this.hasAttribute("interactive") || this.hasAttribute("disabled")) {
2141 return;
2142 }
2143 const target = ev.target;
2144 if (target?.closest(NOCLICK_SELECTOR)) {
2145 return;
2146 }
2147 this._emitCardClick(ev);
2148 };
2149 this._onKeyDown = (ev) => {
2150 if (!this.hasAttribute("interactive") || this.hasAttribute("disabled")) {
2151 return;
2152 }
2153 if (ev.key !== "Enter" && ev.key !== " " && ev.key !== "Spacebar") {
2154 return;
2155 }
2156 const target = ev.target;
2157 if (target && target !== this && target.closest(NOCLICK_SELECTOR)) {
2158 return;
2159 }
2160 if (target instanceof HTMLElement && target !== this && (target.tagName === "BUTTON" || target.tagName === "A" || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT")) {
2161 return;
2162 }
2163 ev.preventDefault();
2164 this._emitCardClick(ev);
2165 };
2166 }
2167 connectedCallback() {
2168 super.connectedCallback();
2169 this._syncRoles();
2170 this.addEventListener("click", this._onClick);
2171 this.addEventListener("keydown", this._onKeyDown);
2172 }
2173 disconnectedCallback() {
2174 this.removeEventListener("click", this._onClick);
2175 this.removeEventListener("keydown", this._onKeyDown);
2176 }
2177 render() {
2178 this._syncRoles();
2179 return html`<slot name="header"></slot><slot></slot><slot name="footer"></slot>`;
2180 }
2181 _syncRoles() {
2182 const interactive = this.hasAttribute("interactive");
2183 const disabled = this.hasAttribute("disabled");
2184 if (interactive) {
2185 if (!this.hasAttribute("role")) {
2186 this.setAttribute("role", "button");
2187 }
2188 this.setAttribute("tabindex", disabled ? "-1" : "0");
2189 this.setAttribute("aria-disabled", disabled ? "true" : "false");
2190 } else {
2191 this.removeAttribute("role");
2192 this.removeAttribute("tabindex");
2193 this.removeAttribute("aria-disabled");
2194 }
2195 }
2196 _emitCardClick(originalEvent) {
2197 this.dispatchEvent(
2198 new CustomEvent("wpd-card-click", {
2199 detail: { originalEvent },
2200 bubbles: true,
2201 composed: true
2202 })
2203 );
2204 }
2205 };
2206 _WpdCard.props = [
2207 "interactive",
2208 "selected",
2209 "compact",
2210 "disabled"
2211 ];
2212 _WpdCard.styles = [styles$b];
2213 _WpdCard.help = {
2214 title: "Card",
2215 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.",
2216 status: "stable",
2217 since: "0.9.0",
2218 props: [
2219 {
2220 name: "interactive",
2221 type: "boolean",
2222 default: "false",
2223 description: 'Surfaces hover lift + cursor + role="button" + emits `wpd-card-click` on click / Enter / Space.'
2224 },
2225 {
2226 name: "selected",
2227 type: "boolean",
2228 default: "false",
2229 description: "Paints the accent ring — pickers / single-selection lists turn this on for the active card."
2230 },
2231 {
2232 name: "compact",
2233 type: "boolean",
2234 default: "false",
2235 description: "Tighter padding + smaller radius for dense lists."
2236 },
2237 {
2238 name: "disabled",
2239 type: "boolean",
2240 default: "false",
2241 description: "Fades the card and blocks pointer / key input. No `wpd-card-click` while disabled."
2242 }
2243 ],
2244 events: [
2245 {
2246 name: "wpd-card-click",
2247 detail: "{ originalEvent: MouseEvent | KeyboardEvent }",
2248 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."
2249 }
2250 ],
2251 slots: [
2252 {
2253 name: "(default)",
2254 description: "Card body. Free-form content."
2255 },
2256 {
2257 name: "header",
2258 description: "Top slot — laid out as a flex row with 12px gap. Drop an `<img>` / icon plus a title block."
2259 },
2260 {
2261 name: "footer",
2262 description: "Bottom slot — pinned via `margin-top: auto`. Standard pattern: meta on the left, primary CTA on the right."
2263 }
2264 ],
2265 cssProps: [
2266 { name: "--wpd-card-bg", default: "#fff" },
2267 { name: "--wpd-card-fg", default: "inherit" },
2268 { name: "--wpd-card-padding", default: "16px" },
2269 { name: "--wpd-card-padding-compact", default: "10px" },
2270 { name: "--wpd-card-gap", default: "12px" },
2271 { name: "--wpd-card-gap-compact", default: "6px" },
2272 { name: "--wpd-card-radius", default: "12px" },
2273 { name: "--wpd-card-radius-compact", default: "8px" },
2274 { name: "--wpd-card-border", default: "var(--wpd-border, rgba(0,0,0,0.08))" },
2275 { name: "--wpd-card-border-hover", default: "var(--wpd-border-strong, rgba(0,0,0,0.16))" },
2276 { name: "--wpd-card-border-selected", default: "var(--wp-admin-theme-color, #2271b1)" },
2277 { name: "--wpd-card-shadow-hover", default: "0 4px 16px rgba(0,0,0,0.08)" }
2278 ],
2279 example: html`
2280 <wpd-card interactive>
2281 <header>
2282 <wpd-icon name="dashicons-admin-plugins" size="40"></wpd-icon>
2283 <div>
2284 <h3>Akismet</h3>
2285 <p>by Automattic</p>
2286 </div>
2287 </header>
2288 <p>The anti-spam service for WordPress sites.</p>
2289 <footer>
2290 <span>1M+ active</span>
2291 <wpd-button variant="primary" data-noclick>Install</wpd-button>
2292 </footer>
2293 </wpd-card>
2294 `
2295 };
2296 let WpdCard = _WpdCard;
2297 defineComponent("wpd-card", WpdCard);
2298 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}`;
2299 const _WpdBadge = class _WpdBadge extends Component {
2300 render() {
2301 return html`<span class="dot" aria-hidden="true"></span><slot></slot>`;
2302 }
2303 };
2304 _WpdBadge.props = ["tone", "noDot"];
2305 _WpdBadge.styles = [styles$a];
2306 _WpdBadge.help = {
2307 title: "Badge",
2308 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.",
2309 status: "experimental",
2310 since: "0.6.0",
2311 props: [
2312 {
2313 name: "tone",
2314 type: '"success" | "warning" | "danger" | "info" | "neutral"',
2315 description: 'Color tone applied to the dot + pill background. Default is "neutral".'
2316 },
2317 {
2318 name: "no-dot",
2319 type: "boolean",
2320 description: "Suppress the leading dot. Useful for count badges where the label itself is the whole signal."
2321 }
2322 ],
2323 slots: [{ name: "(default)", description: "Badge label." }],
2324 cssProps: [
2325 { name: "--wpd-badge-color", description: "Foreground color (also dot color)." },
2326 { name: "--wpd-badge-bg", description: "Pill background color." },
2327 { name: "--wpd-badge-border", default: "1px solid transparent" },
2328 { name: "--wpd-badge-padding", default: "2px 8px" },
2329 { name: "--wpd-badge-gap", default: "6px" },
2330 { name: "--wpd-badge-dot-size", default: "8px" },
2331 { name: "--wpd-badge-border-radius", default: "999px" }
2332 ],
2333 example: html`
2334 <wpd-badge tone="success">Attached</wpd-badge>
2335 <wpd-badge tone="warning">Detaching…</wpd-badge>
2336 <wpd-badge tone="danger">Errored</wpd-badge>
2337 <wpd-badge tone="info" no-dot>v0.6.0</wpd-badge>
2338 `
2339 };
2340 let WpdBadge = _WpdBadge;
2341 defineComponent("wpd-badge", WpdBadge);
2342 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}}`;
2343 const FOCUSABLE_SELECTOR = [
2344 "a[href]",
2345 "area[href]",
2346 "button:not([disabled])",
2347 'input:not([disabled]):not([type="hidden"])',
2348 "select:not([disabled])",
2349 "textarea:not([disabled])",
2350 '[tabindex]:not([tabindex="-1"])',
2351 '[contenteditable="true"]'
2352 ].join(",");
2353 const CLOSE_BUTTON_SELECTOR = "[data-flyout-close]";
2354 const _WpdFlyout = class _WpdFlyout extends Component {
2355 constructor() {
2356 super(...arguments);
2357 this._trigger = null;
2358 this._onDocKey = null;
2359 this._onScopePointerDown = null;
2360 this._onHostClick = null;
2361 this._onHostKeyDown = null;
2362 this._pendingReason = null;
2363 }
2364 connectedCallback() {
2365 super.connectedCallback();
2366 if (!this.hasAttribute("role")) {
2367 this.setAttribute("role", "dialog");
2368 }
2369 if (!this.hasAttribute("open")) {
2370 this.setAttribute("inert", "");
2371 }
2372 }
2373 disconnectedCallback() {
2374 this._detachOpenListeners();
2375 }
2376 attributeChangedCallback(name, oldValue, newValue) {
2377 super.attributeChangedCallback(name, oldValue, newValue);
2378 if (name === "open") {
2379 if (newValue !== null && oldValue === null) {
2380 this._handleOpen();
2381 } else if (newValue === null && oldValue !== null) {
2382 this._handleClose();
2383 }
2384 }
2385 }
2386 _handleOpen() {
2387 this.removeAttribute("inert");
2388 const doc = this.ownerDocument;
2389 const active = doc?.activeElement ?? null;
2390 this._trigger = active instanceof HTMLElement && active !== this && active !== doc?.body && active !== doc?.documentElement ? active : null;
2391 queueMicrotask(() => {
2392 if (!this.hasAttribute("open")) {
2393 return;
2394 }
2395 const focusable = this._firstFocusable();
2396 (focusable ?? this).focus?.({ preventScroll: true });
2397 });
2398 this._attachOpenListeners();
2399 }
2400 _handleClose() {
2401 const reason = this._pendingReason ?? "api";
2402 this._pendingReason = null;
2403 this.setAttribute("inert", "");
2404 this._detachOpenListeners();
2405 this.emit("wpd-flyout-dismiss", { reason });
2406 const trigger = this._trigger;
2407 this._trigger = null;
2408 if (trigger && trigger.isConnected) {
2409 trigger.focus?.({ preventScroll: true });
2410 }
2411 }
2412 /** Internal dismissal — flags the reason then removes `open`. */
2413 _dismiss(reason) {
2414 if (!this.hasAttribute("open")) {
2415 return;
2416 }
2417 this._pendingReason = reason;
2418 this.removeAttribute("open");
2419 }
2420 _attachOpenListeners() {
2421 const scopeRoot = this._resolveScopeRoot();
2422 this._onScopePointerDown = (e) => {
2423 if (!this.hasAttribute("open")) {
2424 return;
2425 }
2426 const path = e.composedPath();
2427 if (path.includes(this)) {
2428 return;
2429 }
2430 if (this._trigger && path.includes(this._trigger)) {
2431 return;
2432 }
2433 this._dismiss("pointer");
2434 };
2435 scopeRoot.addEventListener("pointerdown", this._onScopePointerDown);
2436 this._onDocKey = (e) => {
2437 if (e.key === "Escape" && this.hasAttribute("open")) {
2438 e.preventDefault();
2439 this._dismiss("escape");
2440 }
2441 };
2442 document.addEventListener("keydown", this._onDocKey);
2443 this._onHostKeyDown = (e) => {
2444 if (e.key !== "Tab") {
2445 return;
2446 }
2447 const focusables = this._allFocusable();
2448 if (focusables.length === 0) {
2449 e.preventDefault();
2450 return;
2451 }
2452 const first = focusables[0];
2453 const last = focusables[focusables.length - 1];
2454 const active = this.ownerDocument?.activeElement ?? null;
2455 if (e.shiftKey && (active === first || active === this)) {
2456 e.preventDefault();
2457 last.focus({ preventScroll: true });
2458 } else if (!e.shiftKey && active === last) {
2459 e.preventDefault();
2460 first.focus({ preventScroll: true });
2461 }
2462 };
2463 this.addEventListener("keydown", this._onHostKeyDown);
2464 this._onHostClick = (e) => {
2465 const target = e.target;
2466 const closeBtn = target?.closest?.(CLOSE_BUTTON_SELECTOR);
2467 if (closeBtn && this.contains(closeBtn)) {
2468 this._dismiss("close-button");
2469 }
2470 };
2471 this.addEventListener("click", this._onHostClick);
2472 }
2473 _detachOpenListeners() {
2474 if (this._onDocKey) {
2475 document.removeEventListener("keydown", this._onDocKey);
2476 this._onDocKey = null;
2477 }
2478 if (this._onScopePointerDown) {
2479 const scopeRoot = this._resolveScopeRoot();
2480 scopeRoot.removeEventListener("pointerdown", this._onScopePointerDown);
2481 this._onScopePointerDown = null;
2482 }
2483 if (this._onHostKeyDown) {
2484 this.removeEventListener("keydown", this._onHostKeyDown);
2485 this._onHostKeyDown = null;
2486 }
2487 if (this._onHostClick) {
2488 this.removeEventListener("click", this._onHostClick);
2489 this._onHostClick = null;
2490 }
2491 }
2492 _resolveScopeRoot() {
2493 const scope = this.getAttribute("scope") ?? "window";
2494 if (scope === "document") {
2495 return document.body;
2496 }
2497 if (scope === "parent") {
2498 return this.parentElement ?? document.body;
2499 }
2500 const windowBody = this.closest(".desktop-mode-window__body");
2501 return windowBody ?? this.parentElement ?? document.body;
2502 }
2503 _firstFocusable() {
2504 const slotMatch = this.querySelector(FOCUSABLE_SELECTOR);
2505 return slotMatch ?? null;
2506 }
2507 _allFocusable() {
2508 return Array.from(
2509 this.querySelectorAll(FOCUSABLE_SELECTOR)
2510 ).filter((el) => !el.disabled);
2511 }
2512 render() {
2513 return html`<slot></slot>`;
2514 }
2515 };
2516 _WpdFlyout.props = [
2517 "open",
2518 "placement",
2519 "scope",
2520 "aria-label",
2521 "aria-labelledby"
2522 ];
2523 _WpdFlyout.styles = [flyoutStyles];
2524 _WpdFlyout.help = {
2525 title: "Flyout",
2526 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 click target 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.",
2527 status: "experimental",
2528 since: "0.8.2",
2529 props: [
2530 {
2531 name: "open",
2532 type: "boolean attribute",
2533 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`."
2534 },
2535 {
2536 name: "placement",
2537 type: "'end' | 'start' | 'top'",
2538 default: "end",
2539 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."
2540 },
2541 {
2542 name: "scope",
2543 type: "'window' | 'parent' | 'document'",
2544 default: "window",
2545 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."
2546 },
2547 {
2548 name: "aria-label",
2549 type: "string",
2550 description: "Accessible name for the dialog landmark."
2551 },
2552 {
2553 name: "aria-labelledby",
2554 type: "id reference",
2555 description: "Id of the element labelling the flyout — wins over `aria-label` when both are set."
2556 }
2557 ],
2558 events: [
2559 {
2560 name: "wpd-flyout-dismiss",
2561 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."
2562 }
2563 ],
2564 cssProps: [
2565 {
2566 name: "--wpd-flyout-bg",
2567 description: "Card background. Default: white surface."
2568 },
2569 {
2570 name: "--wpd-flyout-fg",
2571 description: "Card foreground. Default: `--desktop-mode-fg`."
2572 },
2573 {
2574 name: "--wpd-flyout-shadow",
2575 description: "Drop shadow. Default: a deep navy 16px/48px lift."
2576 },
2577 {
2578 name: "--wpd-flyout-backdrop",
2579 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."
2580 }
2581 ],
2582 slots: [
2583 {
2584 name: "(default)",
2585 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."
2586 }
2587 ],
2588 example: html`
2589 <div
2590 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;"
2591 >
2592 <div
2593 style="height:32px;background:rgba(0,0,0,0.04);display:flex;align-items:center;padding:0 12px;font-size:12px;opacity:0.7;"
2594 >
2595 Mock window — title bar
2596 </div>
2597 <div style="padding:12px;">
2598 <wpd-button
2599 id="wpd-flyout-example-trigger"
2600 @click=${(e) => {
2601 const flyout = e.currentTarget.getRootNode().querySelector("#wpd-flyout-example-flyout");
2602 flyout?.setAttribute("open", "");
2603 }}
2604 >Open flyout</wpd-button
2605 >
2606 <p style="opacity:0.7;font-size:13px;">
2607 Click the button. The card slides in from the right
2608 edge with margins from every window edge — title
2609 bar stays visible above. Press Escape, click outside
2610 the card, or hit Close to dismiss.
2611 </p>
2612 </div>
2613 <wpd-flyout
2614 id="wpd-flyout-example-flyout"
2615 placement="end"
2616 scope="parent"
2617 aria-label="Sample flyout"
2618 >
2619 <div style="padding:18px;">
2620 <h4 style="margin:0 0 8px;">Account</h4>
2621 <p style="margin:0 0 12px;font-size:13px;opacity:0.8;">
2622 Floating card inside the window. Margins from
2623 every edge so the chrome reads through.
2624 </p>
2625 <wpd-button data-flyout-close>Close</wpd-button>
2626 </div>
2627 </wpd-flyout>
2628 </div>
2629 `
2630 };
2631 let WpdFlyout = _WpdFlyout;
2632 defineComponent("wpd-flyout", WpdFlyout);
2633 function getWpHooks() {
2634 const hooks = window.wp?.hooks;
2635 if (!hooks) {
2636 throw new Error(
2637 "[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."
2638 );
2639 }
2640 return hooks;
2641 }
2642 function addAction(hookName2, namespace, callback, priority) {
2643 getWpHooks().addAction(
2644 hookName2,
2645 namespace,
2646 callback,
2647 priority
2648 );
2649 }
2650 function removeAction(hookName2, namespace) {
2651 return getWpHooks().removeAction(hookName2, namespace);
2652 }
2653 function applyFilters(hookName2, value, ...args) {
2654 return getWpHooks().applyFilters(hookName2, value, ...args);
2655 }
2656 function doAction(hookName2, ...args) {
2657 getWpHooks().doAction(hookName2, ...args);
2658 }
2659 const HOOKS = {
2660 /**
2661 * Action, fires when one of the shell's own try/catch barriers
2662 * catches an exception. Payload: `{ scope:
2663 * 'widget-mount' | 'widget-teardown' | 'window-open' | 'wallpaper-mount' |
2664 * 'wallpaper-teardown' | 'session-save' | 'menu-refresh' | string,
2665 * id?: string, error: unknown }`. Paired with the existing
2666 * `console.error` calls — a monitor widget can surface these as
2667 * first-class entries.
2668 */
2669 SHELL_ERROR: "desktop-mode.shell.error",
2670 /**
2671 * Action, fires once per `wp.desktop.broadcast()` call with the
2672 * fully-resolved `{ topic, payload }` detail. Lets plugins log,
2673 * mirror, or augment broadcast traffic without subscribing for
2674 * every individual topic.
2675 */
2676 BROADCAST: "desktop-mode.broadcast"
2677 };
2678 const HOOK_PREFIX = "desktop-mode.activity.";
2679 function hookName(channel) {
2680 return `${HOOK_PREFIX}${String(channel)}`;
2681 }
2682 let subscribeSeq = 0;
2683 const activity = {
2684 publish(channel, payload) {
2685 doAction(hookName(channel), payload);
2686 },
2687 subscribe(channel, cb) {
2688 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
2689 const hook = hookName(channel);
2690 addAction(
2691 hook,
2692 ns,
2693 (payload) => cb(payload)
2694 );
2695 let removed = false;
2696 return () => {
2697 if (removed) {
2698 return;
2699 }
2700 removed = true;
2701 removeAction(hook, ns);
2702 };
2703 },
2704 filter(channel, value, ...args) {
2705 return applyFilters(hookName(channel), value, ...args);
2706 }
2707 };
2708 const EVENT_NAME = "desktop-mode-broadcast";
2709 function broadcast(topic, payload) {
2710 const filteredTopic = String(
2711 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
2712 );
2713 const filteredPayload = applyFilters(
2714 "desktop-mode.broadcast.payload",
2715 payload,
2716 { topic: filteredTopic }
2717 );
2718 const detail = {
2719 topic: filteredTopic,
2720 payload: filteredPayload
2721 };
2722 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
2723 doAction(HOOKS.BROADCAST, detail);
2724 activity.publish(
2725 filteredTopic,
2726 filteredPayload
2727 );
2728 {
2729 return;
2730 }
2731 }
2732 function subscribe(topic, cb) {
2733 const handler = (e) => {
2734 const detail = e.detail;
2735 if (!detail) {
2736 return;
2737 }
2738 if (detail.topic !== topic) {
2739 return;
2740 }
2741 try {
2742 cb(detail.payload, { topic: detail.topic });
2743 } catch (err) {
2744 doAction(HOOKS.SHELL_ERROR, {
2745 scope: "broadcast-subscriber",
2746 topic: detail.topic,
2747 error: err
2748 });
2749 }
2750 };
2751 document.addEventListener(EVENT_NAME, handler);
2752 return () => document.removeEventListener(EVENT_NAME, handler);
2753 }
2754 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: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}`;
2755 const _WpdButton = class _WpdButton extends Component {
2756 render() {
2757 const disabled = this.disabled !== null;
2758 const type = this.type || "button";
2759 return html`
2760 <button part="button" type=${type} ?disabled=${disabled}>
2761 <slot></slot>
2762 </button>
2763 `;
2764 }
2765 };
2766 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
2767 _WpdButton.styles = [styles$9];
2768 _WpdButton.help = {
2769 title: "Button",
2770 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
2771 status: "stable",
2772 since: "0.9.0",
2773 props: [
2774 {
2775 name: "variant",
2776 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
2777 default: "ghost",
2778 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
2779 },
2780 {
2781 name: "disabled",
2782 type: "boolean attribute",
2783 description: "Disable pointer + keyboard interaction and dim the chrome."
2784 },
2785 {
2786 name: "type",
2787 type: "'button' | 'submit' | 'reset'",
2788 default: "button",
2789 description: "Forwarded to the underlying native <button>."
2790 },
2791 {
2792 name: "busy",
2793 type: "boolean attribute",
2794 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
2795 },
2796 {
2797 name: "fill-cell",
2798 type: "boolean attribute",
2799 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
2800 }
2801 ],
2802 slots: [{ name: "(default)", description: "Button label." }],
2803 parts: [{ name: "button", description: "Underlying <button> element." }],
2804 cssProps: [
2805 { name: "--wpd-button-bg", description: "Background color." },
2806 { name: "--wpd-button-fg", description: "Text color." },
2807 { name: "--wpd-button-border", description: "Border shorthand." },
2808 { name: "--wpd-button-border-radius", default: "6px" },
2809 { name: "--wpd-button-padding", default: "6px 12px" },
2810 {
2811 name: "--wpd-button-min-height",
2812 description: "Minimum height when fill-cell is set."
2813 }
2814 ],
2815 example: html`
2816 <wpd-cluster gap="8">
2817 <wpd-button variant="primary">Primary</wpd-button>
2818 <wpd-button variant="secondary">Secondary</wpd-button>
2819 <wpd-button variant="ghost">Ghost</wpd-button>
2820 <wpd-button variant="danger">Danger</wpd-button>
2821 <wpd-button variant="link">Link</wpd-button>
2822 </wpd-cluster>
2823 `
2824 };
2825 let WpdButton = _WpdButton;
2826 defineComponent("wpd-button", WpdButton);
2827 function buildCard(plugin, installed, callbacks) {
2828 const card = document.createElement("wpd-card");
2829 card.classList.add("desktop-mode-plugins__card");
2830 card.setAttribute("interactive", "");
2831 card.dataset.slug = plugin.slug;
2832 card.setAttribute(
2833 "aria-label",
2834 sprintf(
2835 /* translators: %s: plugin name */
2836 __("View details for %s", "desktop-mode"),
2837 plugin.name
2838 )
2839 );
2840 const header = document.createElement("header");
2841 header.className = "desktop-mode-plugins__card-header";
2842 const iconWrap = document.createElement("div");
2843 iconWrap.className = "desktop-mode-plugins__card-icon";
2844 const iconUrl = pickIcon(plugin.icons);
2845 if (iconUrl) {
2846 const img = document.createElement("img");
2847 img.src = iconUrl;
2848 img.alt = "";
2849 img.loading = "lazy";
2850 img.decoding = "async";
2851 img.addEventListener(
2852 "load",
2853 () => img.classList.add("is-loaded")
2854 );
2855 img.addEventListener("error", () => {
2856 iconWrap.replaceChildren(buildFallbackGlyph$1());
2857 });
2858 iconWrap.appendChild(img);
2859 } else {
2860 iconWrap.appendChild(buildFallbackGlyph$1());
2861 }
2862 const titleBlock = document.createElement("div");
2863 titleBlock.className = "desktop-mode-plugins__card-titleblock";
2864 const title = document.createElement("h3");
2865 title.className = "desktop-mode-plugins__card-title";
2866 title.textContent = decodeEntities(plugin.name);
2867 const byline = document.createElement("p");
2868 byline.className = "desktop-mode-plugins__card-byline";
2869 byline.innerHTML = sprintf(
2870 /* translators: %s: plugin author name (HTML-stripped) */
2871 __("by %s", "desktop-mode"),
2872 `<span>${escapeHtml$2(stripHtml$4(plugin.author ?? ""))}</span>`
2873 );
2874 titleBlock.append(title, byline);
2875 header.setAttribute("slot", "header");
2876 header.append(iconWrap, titleBlock);
2877 const desc = document.createElement("p");
2878 desc.className = "desktop-mode-plugins__card-desc";
2879 desc.textContent = decodeEntities(plugin.short_description ?? "");
2880 const footer = document.createElement("footer");
2881 footer.className = "desktop-mode-plugins__card-footer";
2882 footer.setAttribute("slot", "footer");
2883 const meta = document.createElement("div");
2884 meta.className = "desktop-mode-plugins__card-meta";
2885 meta.appendChild(buildStarCluster(plugin.rating ?? 0, plugin.num_ratings ?? 0));
2886 const installs = document.createElement("span");
2887 installs.className = "desktop-mode-plugins__card-installs";
2888 installs.textContent = formatInstalls(plugin.active_installs ?? 0);
2889 meta.appendChild(installs);
2890 const cta = buildCta(plugin, installed, callbacks, card);
2891 footer.append(meta, cta);
2892 card.append(header, desc, footer);
2893 card.addEventListener("wpd-card-click", () => {
2894 callbacks.onOpen(plugin.slug, plugin);
2895 });
2896 return card;
2897 }
2898 function repaintCardCta(card, plugin, installed, callbacks) {
2899 const footer = card.querySelector(
2900 ".desktop-mode-plugins__card-footer"
2901 );
2902 if (!footer) {
2903 return;
2904 }
2905 const previous = footer.querySelector(
2906 "[data-plugin-card-cta]"
2907 );
2908 if (previous) {
2909 previous.remove();
2910 }
2911 footer.appendChild(buildCta(plugin, installed, callbacks, card));
2912 }
2913 function buildCta(plugin, installed, callbacks, card) {
2914 const installedRow = installed.get(plugin.slug);
2915 const button2 = document.createElement("wpd-button");
2916 button2.setAttribute("data-plugin-card-cta", "");
2917 button2.setAttribute("data-noclick", "");
2918 if (installedRow) {
2919 if (installedRow.status === "active" || installedRow.status === "active-network") {
2920 button2.setAttribute("variant", "ghost");
2921 button2.setAttribute("disabled", "");
2922 button2.textContent = __("Active", "desktop-mode");
2923 } else {
2924 button2.setAttribute("variant", "primary");
2925 button2.textContent = __("Activate", "desktop-mode");
2926 button2.addEventListener("click", (ev) => {
2927 ev.stopPropagation();
2928 void callbacks.onActivate(installedRow, card);
2929 });
2930 }
2931 } else {
2932 button2.setAttribute("variant", "primary");
2933 button2.textContent = __("Install", "desktop-mode");
2934 button2.addEventListener("click", (ev) => {
2935 ev.stopPropagation();
2936 void callbacks.onInstall(plugin, card);
2937 });
2938 }
2939 return button2;
2940 }
2941 function buildStarCluster(rating0to100, totalRatings) {
2942 const wrap = document.createElement("span");
2943 wrap.className = "desktop-mode-plugins__stars";
2944 wrap.setAttribute("aria-label", formatStarsAriaLabel(rating0to100));
2945 const stars5 = Math.max(0, Math.min(5, rating0to100 / 100 * 5));
2946 const full = Math.floor(stars5);
2947 const half = stars5 - full >= 0.5 ? 1 : 0;
2948 const empty = 5 - full - half;
2949 for (let i = 0; i < full; i++) {
2950 wrap.appendChild(buildStar("filled"));
2951 }
2952 for (let i = 0; i < half; i++) {
2953 wrap.appendChild(buildStar("half"));
2954 }
2955 for (let i = 0; i < empty; i++) {
2956 wrap.appendChild(buildStar("empty"));
2957 }
2958 if (totalRatings > 0) {
2959 const count = document.createElement("span");
2960 count.className = "desktop-mode-plugins__stars-count";
2961 count.textContent = `(${formatThousands(totalRatings)})`;
2962 wrap.appendChild(count);
2963 }
2964 return wrap;
2965 }
2966 function buildStar(kind) {
2967 const span = document.createElement("span");
2968 span.className = "desktop-mode-plugins__star";
2969 span.setAttribute("aria-hidden", "true");
2970 const icon = document.createElement("span");
2971 if (kind === "filled") {
2972 icon.className = "dashicons dashicons-star-filled";
2973 } else if (kind === "half") {
2974 icon.className = "dashicons dashicons-star-half";
2975 } else {
2976 icon.className = "dashicons dashicons-star-empty";
2977 }
2978 span.appendChild(icon);
2979 return span;
2980 }
2981 function buildFallbackGlyph$1() {
2982 const fallback = document.createElement("span");
2983 fallback.className = "dashicons dashicons-admin-plugins desktop-mode-plugins__card-icon-fallback";
2984 fallback.setAttribute("aria-hidden", "true");
2985 return fallback;
2986 }
2987 function pickIcon(icons) {
2988 if (!icons) {
2989 return null;
2990 }
2991 return icons.svg ?? icons["256"] ?? icons["256x256"] ?? icons.default ?? icons["128"] ?? icons["128x128"] ?? icons["2x"] ?? icons["1x"] ?? Object.values(icons)[0] ?? null;
2992 }
2993 function formatInstalls(n) {
2994 if (n <= 0) {
2995 return __("Fewer than 10 active", "desktop-mode");
2996 }
2997 if (n >= 1e6) {
2998 const millions = Math.floor(n / 1e6);
2999 return sprintf(
3000 /* translators: %d: integer number of millions of active installs */
3001 __("%d+ million active", "desktop-mode"),
3002 millions
3003 );
3004 }
3005 if (n >= 1e3) {
3006 return sprintf(
3007 /* translators: %s: comma-grouped active install count */
3008 __("%s+ active", "desktop-mode"),
3009 formatThousands(roundTo3SigFigs(n))
3010 );
3011 }
3012 return sprintf(
3013 /* translators: %s: comma-grouped active install count */
3014 __("%s+ active", "desktop-mode"),
3015 formatThousands(n)
3016 );
3017 }
3018 function roundTo3SigFigs(n) {
3019 const order = Math.pow(10, Math.floor(Math.log10(n)) - 2);
3020 return Math.floor(n / order) * order;
3021 }
3022 function formatStarsAriaLabel(rating0to100) {
3023 const stars5 = Math.max(0, Math.min(5, rating0to100 / 100 * 5));
3024 return sprintf(
3025 /* translators: %s: rating out of 5 (one decimal) */
3026 __("Rated %s out of 5", "desktop-mode"),
3027 stars5.toFixed(1)
3028 );
3029 }
3030 function formatThousands(n) {
3031 try {
3032 return new Intl.NumberFormat().format(n);
3033 } catch {
3034 return String(n);
3035 }
3036 }
3037 const _entityCache = document.createElement("textarea");
3038 function decodeEntities(html2) {
3039 if (!html2) {
3040 return "";
3041 }
3042 _entityCache.innerHTML = html2;
3043 return _entityCache.value;
3044 }
3045 function escapeHtml$2(raw) {
3046 const tmp = document.createElement("div");
3047 tmp.textContent = raw;
3048 return tmp.innerHTML;
3049 }
3050 function stripHtml$4(html2) {
3051 const tmp = document.createElement("div");
3052 tmp.innerHTML = html2;
3053 return tmp.textContent ?? "";
3054 }
3055 function api() {
3056 return window.wp?.desktop ?? null;
3057 }
3058 function makeCardDraggable(card, plugin) {
3059 if (card.dataset.dragWired === "1") {
3060 return;
3061 }
3062 card.dataset.dragWired = "1";
3063 card.addEventListener("pointerdown", (ev) => {
3064 const desktop = api();
3065 const manager = desktop?.dragManager;
3066 if (!manager) {
3067 return;
3068 }
3069 const t = ev.target;
3070 if (t?.closest("[data-plugin-card-cta]")) {
3071 return;
3072 }
3073 manager.start({
3074 payload: {
3075 type: "wporg-plugin",
3076 source: card,
3077 data: {
3078 slug: plugin.slug,
3079 name: plugin.name,
3080 iconUrl: pickIcon(plugin.icons) ?? null,
3081 homepage: plugin.homepage ?? "",
3082 authorName: stripHtml$3(plugin.author ?? ""),
3083 shortDescription: plugin.short_description ?? ""
3084 },
3085 ghost: buildGhost(plugin, card, ev)
3086 },
3087 origin: ev,
3088 onClickOnly: () => {
3089 }
3090 });
3091 });
3092 }
3093 function installPluginDropTargets() {
3094 const desktop = api();
3095 const manager = desktop?.dragManager;
3096 if (!manager) {
3097 return () => {
3098 };
3099 }
3100 const teardowns = [];
3101 const dock = findDockElement();
3102 if (dock) {
3103 const off = manager.registerDropTarget({
3104 id: "desktop-mode-plugins-window/dock",
3105 element: dock,
3106 accept: (p) => p.type === "wporg-plugin",
3107 onEnter: () => {
3108 dock.setAttribute("data-plugins-card-drop-active", "");
3109 },
3110 onLeave: () => {
3111 dock.removeAttribute("data-plugins-card-drop-active");
3112 },
3113 onDrop: (session) => {
3114 dock.removeAttribute("data-plugins-card-drop-active");
3115 const data = session.payload.data;
3116 const slug = String(data.slug ?? "");
3117 if (!slug) {
3118 return;
3119 }
3120 const name = String(data.name ?? slug);
3121 const icon = typeof data.iconUrl === "string" && data.iconUrl ? data.iconUrl : "dashicons-admin-plugins";
3122 const homepage = String(data.homepage ?? "");
3123 const url = homepage !== "" ? homepage : `https://wordpress.org/plugins/${encodeURIComponent(slug)}/`;
3124 if (typeof desktop?.registerSystemTile === "function") {
3125 desktop.registerSystemTile({
3126 id: `wporg-plugin-${slug}`,
3127 title: name,
3128 icon,
3129 url
3130 });
3131 }
3132 if (typeof desktop?.showToast === "function") {
3133 desktop.showToast({
3134 message: sprintf(
3135 /* translators: %s: plugin name */
3136 __("Pinned %s to the dock.", "desktop-mode"),
3137 name
3138 ),
3139 duration: 3500
3140 });
3141 }
3142 }
3143 });
3144 teardowns.push(off);
3145 }
3146 return () => {
3147 for (const off of teardowns) {
3148 try {
3149 off();
3150 } catch {
3151 }
3152 }
3153 };
3154 }
3155 function findDockElement() {
3156 return document.querySelector(".desktop-mode-bottom-dock") ?? document.querySelector(".desktop-mode-dock") ?? document.querySelector("[data-desktop-mode-dock]");
3157 }
3158 function buildGhost(plugin, card, origin) {
3159 const rect = card.getBoundingClientRect();
3160 const offsetX = origin.clientX - rect.left;
3161 const offsetY = origin.clientY - rect.top;
3162 const ghost = document.createElement("div");
3163 ghost.className = "desktop-mode-plugins__drag-ghost";
3164 const iconUrl = pickIcon(plugin.icons);
3165 if (iconUrl) {
3166 const img = document.createElement("img");
3167 img.src = iconUrl;
3168 img.alt = "";
3169 ghost.appendChild(img);
3170 } else {
3171 const fallback = document.createElement("span");
3172 fallback.className = "dashicons dashicons-admin-plugins desktop-mode-plugins__drag-ghost-fallback";
3173 ghost.appendChild(fallback);
3174 }
3175 const label = document.createElement("span");
3176 label.textContent = plugin.name;
3177 ghost.appendChild(label);
3178 return { offsetX, offsetY, element: ghost };
3179 }
3180 function stripHtml$3(html2) {
3181 const tmp = document.createElement("div");
3182 tmp.innerHTML = html2;
3183 return tmp.textContent ?? "";
3184 }
3185 const FALLBACK_BASE = "http://localhost/";
3186 function joinRestUrl(restRoot, path) {
3187 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
3188 const url = new URL(restRoot, base);
3189 const trimmed = path.replace(/^\/+/, "");
3190 const queryAt = trimmed.indexOf("?");
3191 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
3192 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
3193 if (url.searchParams.has("rest_route")) {
3194 const existing = url.searchParams.get("rest_route") ?? "/";
3195 const prefix = existing.endsWith("/") ? existing : existing + "/";
3196 url.searchParams.set("rest_route", prefix + route);
3197 } else {
3198 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
3199 url.pathname = pathname + route;
3200 }
3201 if (extraQuery) {
3202 const extras = new URLSearchParams(extraQuery);
3203 extras.forEach((value, key) => {
3204 url.searchParams.append(key, value);
3205 });
3206 }
3207 return url.toString();
3208 }
3209 const WINDOW_ID = "desktop-mode-plugins";
3210 function getConfig() {
3211 const store = window.desktopModeWindowConfig;
3212 const cfg = store ? store[WINDOW_ID] : void 0;
3213 if (!cfg) {
3214 throw new Error(
3215 `[${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\`.`
3216 );
3217 }
3218 return cfg;
3219 }
3220 function shellFetch(input, init) {
3221 return trackedFetch(input, init, {
3222 windowId: WINDOW_ID,
3223 source: "desktop-mode/plugins-window"
3224 });
3225 }
3226 async function restRequest(url, init = {}) {
3227 const cfg = getConfig();
3228 const { expectJson = true, ...rest } = init;
3229 const response = await shellFetch(url, {
3230 ...rest,
3231 credentials: "same-origin",
3232 headers: {
3233 "X-WP-Nonce": cfg.restNonce,
3234 Accept: "application/json",
3235 ...rest.body ? { "Content-Type": "application/json" } : {},
3236 ...rest.headers ?? {}
3237 }
3238 });
3239 if (!response.ok) {
3240 throw await unpackErrorResponse(response);
3241 }
3242 if (!expectJson) {
3243 return void 0;
3244 }
3245 return await response.json();
3246 }
3247 async function ajaxRequest(action, args = {}, options = {}) {
3248 const cfg = getConfig();
3249 const body = new URLSearchParams();
3250 body.set("action", action);
3251 const nonceField = options.nonceField ?? "_ajax_nonce";
3252 const nonceValue = options.nonceValue ?? cfg.ajaxNonce;
3253 body.set(nonceField, nonceValue);
3254 for (const [key, value] of Object.entries(args)) {
3255 if (value === void 0) {
3256 continue;
3257 }
3258 body.set(key, String(value));
3259 }
3260 const response = await shellFetch(cfg.ajaxUrl, {
3261 method: "POST",
3262 credentials: "same-origin",
3263 headers: {
3264 "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
3265 Accept: "application/json"
3266 },
3267 body
3268 });
3269 const json = await readJsonOrThrow(response);
3270 return unwrapAjaxEnvelope(json, response.status);
3271 }
3272 async function ajaxUpload(action, formData) {
3273 const cfg = getConfig();
3274 formData.set("action", action);
3275 if (!formData.has("_ajax_nonce")) {
3276 formData.set("_ajax_nonce", cfg.ajaxNonce);
3277 }
3278 const response = await shellFetch(cfg.ajaxUrl, {
3279 method: "POST",
3280 credentials: "same-origin",
3281 body: formData
3282 // Don't set Content-Type — the browser appends the boundary.
3283 });
3284 const json = await readJsonOrThrow(response);
3285 return unwrapAjaxEnvelope(json, response.status);
3286 }
3287 async function readJsonOrThrow(response) {
3288 let json;
3289 try {
3290 json = await response.json();
3291 } catch (err) {
3292 throw new Error(
3293 `Server returned ${response.status} with non-JSON body. (${String(err)})`
3294 );
3295 }
3296 if (!response.ok) {
3297 const errPayload = typeof json === "object" && json !== null && "success" in json && json.success === false ? json.data : json;
3298 throw extractAjaxError(errPayload, response.status);
3299 }
3300 return json;
3301 }
3302 function unwrapAjaxEnvelope(json, status) {
3303 if (typeof json === "object" && json !== null && "success" in json) {
3304 const env = json;
3305 if (env.success) {
3306 return env.data ?? null;
3307 }
3308 throw extractAjaxError(env.data, status);
3309 }
3310 return json;
3311 }
3312 function extractAjaxError(data, status) {
3313 if (typeof data === "object" && data !== null) {
3314 const obj = data;
3315 const msg = obj.message ?? obj.errorMessage ?? obj.code ?? obj.errorCode;
3316 if (typeof msg === "string" && msg !== "") {
3317 const err = new Error(msg);
3318 err.code = obj.code ?? obj.errorCode;
3319 err.status = status;
3320 return err;
3321 }
3322 }
3323 return new Error(`Request failed (${status}).`);
3324 }
3325 async function unpackErrorResponse(response) {
3326 let message = `${response.status} ${response.statusText}`;
3327 try {
3328 const json = await response.json();
3329 if (json && typeof json.message === "string" && json.message !== "") {
3330 message = json.message;
3331 }
3332 const err = new Error(message);
3333 err.code = json?.code;
3334 err.status = response.status;
3335 return err;
3336 } catch {
3337 const err = new Error(message);
3338 err.status = response.status;
3339 return err;
3340 }
3341 }
3342 async function fetchInstalledPlugins(opts = {}) {
3343 const cfg = getConfig();
3344 const params = new URLSearchParams({ context: "view", per_page: "100" });
3345 if (opts.force) {
3346 params.set("desktop_mode_force_refresh", "1");
3347 }
3348 const url = joinRestUrl(cfg.restRoot, `wp/v2/plugins?${params.toString()}`);
3349 return restRequest(url, { method: "GET" });
3350 }
3351 async function activateInstalledPlugin(plugin) {
3352 return mutateInstalledPlugin(plugin, { status: "active" });
3353 }
3354 async function deactivateInstalledPlugin(plugin) {
3355 return mutateInstalledPlugin(plugin, { status: "inactive" });
3356 }
3357 async function mutateInstalledPlugin(plugin, body) {
3358 const cfg = getConfig();
3359 return restRequest(
3360 joinRestUrl(cfg.restRoot, `wp/v2/plugins/${encodePluginPath(plugin.plugin)}`),
3361 {
3362 method: "PUT",
3363 body: JSON.stringify(body)
3364 }
3365 );
3366 }
3367 async function deleteInstalledPlugin(plugin) {
3368 const cfg = getConfig();
3369 await restRequest(
3370 joinRestUrl(
3371 cfg.restRoot,
3372 `wp/v2/plugins/${encodePluginPath(plugin.plugin)}?force=true`
3373 ),
3374 {
3375 method: "DELETE",
3376 expectJson: false
3377 }
3378 );
3379 }
3380 function encodePluginPath(plugin) {
3381 return plugin.split("/").map(encodeURIComponent).join("/");
3382 }
3383 async function updateInstalledPlugin(plugin) {
3384 const pluginFile = plugin.plugin.endsWith(".php") ? plugin.plugin : plugin.plugin + ".php";
3385 return ajaxRequest(
3386 "update-plugin",
3387 {
3388 plugin: pluginFile,
3389 slug: plugin.desktop_mode_update_available?.slug || plugin.textdomain || plugin.plugin.split("/")[0]
3390 },
3391 { nonceField: "_ajax_nonce", nonceValue: getConfig().updatesNonce }
3392 );
3393 }
3394 async function toggleAutoUpdate(plugin, state) {
3395 const pluginFile = plugin.plugin.endsWith(".php") ? plugin.plugin : plugin.plugin + ".php";
3396 await ajaxRequest(
3397 "toggle-auto-updates",
3398 {
3399 type: "plugin",
3400 asset: pluginFile,
3401 state
3402 },
3403 { nonceField: "_ajax_nonce", nonceValue: getConfig().updatesNonce }
3404 );
3405 }
3406 async function browsePlugins(args = {}) {
3407 return ajaxRequest("desktop_mode_plugins_browse", {
3408 browse: args.browse,
3409 search: args.search,
3410 tag: args.tag,
3411 page: args.page,
3412 per_page: args.perPage
3413 });
3414 }
3415 async function fetchPluginInfo(slug) {
3416 return ajaxRequest("desktop_mode_plugins_info", { slug });
3417 }
3418 async function fetchFeaturedPlugins() {
3419 return ajaxRequest("desktop_mode_plugins_featured");
3420 }
3421 async function fetchPluginReviews(slug) {
3422 return ajaxRequest("desktop_mode_plugins_reviews", { slug });
3423 }
3424 async function installPluginBySlug(slug) {
3425 return ajaxRequest(
3426 "install-plugin",
3427 { slug },
3428 { nonceField: "_ajax_nonce", nonceValue: getConfig().updatesNonce }
3429 );
3430 }
3431 async function uploadPluginZip(file, options = {}) {
3432 const data = new FormData();
3433 data.set("pluginzip", file);
3434 if (options.overwrite) {
3435 data.set("overwrite", "1");
3436 }
3437 return ajaxUpload("desktop_mode_plugins_upload", data);
3438 }
3439 async function refreshFrameworkMenu() {
3440 const refresh = window.wp?.desktop?.refreshMenu;
3441 if (typeof refresh !== "function") {
3442 return;
3443 }
3444 try {
3445 await refresh();
3446 } catch {
3447 }
3448 }
3449 function isDesktopModeSelf(pluginFile) {
3450 let self = "";
3451 try {
3452 self = getConfig().selfPluginFile;
3453 } catch {
3454 return false;
3455 }
3456 const trim = (s) => s.endsWith(".php") ? s.slice(0, -4) : s;
3457 return self !== "" && trim(self) === trim(pluginFile);
3458 }
3459 function reloadOutOfDesktopMode() {
3460 const target = window.top ?? window;
3461 let dest;
3462 try {
3463 dest = getConfig().adminUrl;
3464 } catch {
3465 dest = "";
3466 }
3467 window.setTimeout(() => {
3468 if (dest) {
3469 try {
3470 target.location.assign(dest);
3471 return;
3472 } catch {
3473 }
3474 window.location.assign(dest);
3475 return;
3476 }
3477 try {
3478 target.location.reload();
3479 } catch {
3480 window.location.reload();
3481 }
3482 }, 800);
3483 }
3484 const tabsStyles = css`:host{display:flex;gap:4px;margin-bottom:10px;border-bottom:1px solid var( --desktop-mode-border,#dcdcde )}`;
3485 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}`;
3486 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 )}`;
3487 const _WpdTab = class _WpdTab extends Component {
3488 render() {
3489 this.setAttribute("role", "tab");
3490 return html`
3491 <button type="button" @click=${() => this._onPick()}>
3492 <slot></slot>
3493 </button>
3494 `;
3495 }
3496 _onPick() {
3497 this.emit("wpd-tab-pick", {
3498 value: this.value
3499 });
3500 }
3501 };
3502 _WpdTab.props = ["value"];
3503 _WpdTab.styles = [tabStyles];
3504 _WpdTab.help = {
3505 title: "Tab",
3506 summary: "Single tab inside a <wpd-tabs> strip. Carries its identifier via `value`; aria-selected + tabindex are mirrored by the parent.",
3507 status: "stable",
3508 since: "0.7.0",
3509 props: [
3510 {
3511 name: "value",
3512 type: "string",
3513 description: "Identifier the tab contributes to the parent strip selection."
3514 }
3515 ],
3516 slots: [
3517 { name: "(default)", description: "Visible tab label." }
3518 ],
3519 events: [
3520 {
3521 name: "wpd-tab-pick",
3522 description: "Internal event bubbled to the parent <wpd-tabs>. Consumers should listen for wpd-tab-change on the strip instead.",
3523 detail: "{ value: string | null }"
3524 }
3525 ]
3526 };
3527 let WpdTab = _WpdTab;
3528 defineComponent("wpd-tab", WpdTab);
3529 const _WpdTabs = class _WpdTabs extends Component {
3530 connectedCallback() {
3531 super.connectedCallback();
3532 this.addEventListener("wpd-tab-pick", (e) => {
3533 const detail = e.detail;
3534 e.stopPropagation();
3535 this.value = detail.value;
3536 this.emit("wpd-tab-change", { value: detail.value });
3537 });
3538 }
3539 /**
3540 * Declarative item-list setter. Replaces the existing `<wpd-tab>`
3541 * children with a fresh set built from a `{ value, label }`
3542 * array. The `value` prop is preserved if it still matches a new
3543 * entry; otherwise it falls back to the first item.
3544 *
3545 * Lets plugins that populate tabs dynamically (route-driven
3546 * admin screens, filtered lists) replace the declarative
3547 * markup with a one-liner:
3548 *
3549 * ```js
3550 * tabs.items = [
3551 * { value: 'calc', label: 'Calc' },
3552 * { value: 'convert', label: 'Convert' },
3553 * ];
3554 * ```
3555 *
3556 * @since 0.11.0
3557 */
3558 set items(list) {
3559 replaceChildren(this, "wpd-tab", list);
3560 const current = this.value;
3561 const stillValid = current !== null && list.some((i) => i.value === current);
3562 if (!stillValid && list.length > 0) {
3563 this.value = list[0].value;
3564 } else {
3565 this.requestUpdate();
3566 }
3567 }
3568 render() {
3569 this.setAttribute("role", "tablist");
3570 const label = this.label || "";
3571 if (label) {
3572 this.setAttribute("aria-label", label);
3573 }
3574 const current = this.value;
3575 queueMicrotask(() => {
3576 const tabs = this.querySelectorAll("wpd-tab");
3577 for (const tab of Array.from(tabs)) {
3578 const v = tab.getAttribute("value");
3579 tab.setAttribute(
3580 "aria-selected",
3581 v === current ? "true" : "false"
3582 );
3583 tab.setAttribute("tabindex", v === current ? "0" : "-1");
3584 }
3585 syncTabpanels(this, current);
3586 });
3587 return html`<slot></slot>`;
3588 }
3589 };
3590 _WpdTabs.props = ["value", "label"];
3591 _WpdTabs.styles = [tabsStyles];
3592 _WpdTabs.help = {
3593 title: "Tabs",
3594 summary: 'Underline-accent tab strip. Pair with sibling <wpd-tabpanel for="…"> elements and the strip auto-toggles their hidden attribute on selection.',
3595 status: "stable",
3596 since: "0.7.0",
3597 props: [
3598 {
3599 name: "value",
3600 type: "string",
3601 description: "Currently active tab value. Mirrored to child <wpd-tab> aria-selected."
3602 },
3603 {
3604 name: "label",
3605 type: "string",
3606 description: "aria-label for the tablist — describe the tab group for assistive tech."
3607 }
3608 ],
3609 slots: [
3610 {
3611 name: "(default)",
3612 description: '<wpd-tab value="…"> children forming the strip.'
3613 }
3614 ],
3615 events: [
3616 {
3617 name: "wpd-tab-change",
3618 description: "Fires when the active tab changes.",
3619 detail: "{ value: string }"
3620 }
3621 ],
3622 example: html`
3623 <wpd-tabs value="one" label="Demo tabs">
3624 <wpd-tab value="one">One</wpd-tab>
3625 <wpd-tab value="two">Two</wpd-tab>
3626 <wpd-tab value="three">Three</wpd-tab>
3627 </wpd-tabs>
3628 <wpd-tabpanel for="one">First panel.</wpd-tabpanel>
3629 <wpd-tabpanel for="two">Second panel.</wpd-tabpanel>
3630 <wpd-tabpanel for="three">Third panel.</wpd-tabpanel>
3631 `
3632 };
3633 let WpdTabs = _WpdTabs;
3634 defineComponent("wpd-tabs", WpdTabs);
3635 const _WpdTabPanel = class _WpdTabPanel extends Component {
3636 // Shadow DOM — the render target for this component is its
3637 // own shadow root, which holds a single `<slot>` that projects
3638 // whatever the caller placed between the `<wpd-tabpanel>` open
3639 // and close tags. Slotted children remain light-DOM descendants
3640 // of the panel element (the slot rendering mechanism doesn't
3641 // move them), so `panel.querySelector(...)` from plugin render
3642 // callbacks keeps working.
3643 //
3644 // Earlier 0.11.0 builds of this component used light DOM with
3645 // a `<slot>` render, which wiped the panel's server-rendered
3646 // template content on first mount — every `render()` writes
3647 // into `_renderRoot`, and with light DOM that's the panel
3648 // itself. Shadow DOM isolates the render surface.
3649 connectedCallback() {
3650 super.connectedCallback();
3651 this.setAttribute("role", "tabpanel");
3652 if (!this.hasAttribute("tabindex")) {
3653 this.setAttribute("tabindex", "0");
3654 }
3655 const owner = findOwningTabs(this);
3656 if (owner) {
3657 syncTabpanels(owner, owner.getAttribute("value"));
3658 }
3659 }
3660 render() {
3661 return html`<slot></slot>`;
3662 }
3663 };
3664 _WpdTabPanel.props = ["for"];
3665 _WpdTabPanel.styles = [tabPanelStyles];
3666 _WpdTabPanel.help = {
3667 title: "Tab panel",
3668 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.',
3669 status: "stable",
3670 since: "0.11.0",
3671 props: [
3672 {
3673 name: "for",
3674 type: "string",
3675 description: "Matches the `value` of the owning <wpd-tab>. Panel is shown when its parent tabs strip is on that value."
3676 }
3677 ],
3678 slots: [
3679 { name: "(default)", description: "Panel body content." }
3680 ]
3681 };
3682 let WpdTabPanel = _WpdTabPanel;
3683 defineComponent("wpd-tabpanel", WpdTabPanel);
3684 function replaceChildren(host, tag, items) {
3685 const existing = host.querySelectorAll(`:scope > ${tag}`);
3686 for (const el of Array.from(existing)) {
3687 el.remove();
3688 }
3689 for (const item of items) {
3690 const el = document.createElement(tag);
3691 el.setAttribute("value", item.value);
3692 el.textContent = item.label;
3693 host.appendChild(el);
3694 }
3695 }
3696 function findOwningTabs(panel) {
3697 const parent = panel.parentElement;
3698 if (!parent) {
3699 return null;
3700 }
3701 const sibling = parent.querySelector(":scope > wpd-tabs");
3702 if (sibling) {
3703 return sibling;
3704 }
3705 return panel.closest("wpd-tabs");
3706 }
3707 function syncTabpanels(tabs, value) {
3708 const panels = /* @__PURE__ */ new Set();
3709 const parent = tabs.parentElement;
3710 if (parent) {
3711 for (const p of Array.from(
3712 parent.querySelectorAll(":scope > wpd-tabpanel")
3713 )) {
3714 panels.add(p);
3715 }
3716 }
3717 for (const p of Array.from(
3718 tabs.querySelectorAll(":scope > wpd-tabpanel")
3719 )) {
3720 panels.add(p);
3721 }
3722 for (const panel of panels) {
3723 const pfor = panel.getAttribute("for");
3724 const active = pfor !== null && pfor === value;
3725 if (active) {
3726 panel.removeAttribute("hidden");
3727 } else {
3728 panel.setAttribute("hidden", "");
3729 }
3730 panel.setAttribute("aria-hidden", active ? "false" : "true");
3731 }
3732 }
3733 function toast$3(message, duration = 3500) {
3734 const api2 = window.wp?.desktop;
3735 if (api2 && typeof api2.showToast === "function") {
3736 api2.showToast({ message, duration });
3737 return;
3738 }
3739 console.log("[plugins-window]", message);
3740 }
3741 async function confirm$1(opts) {
3742 const api2 = window.wp?.desktop;
3743 if (api2 && typeof api2.confirm === "function") {
3744 return api2.confirm(opts);
3745 }
3746 return Promise.resolve(true);
3747 }
3748 function openDetailFlyout(flyout, slug, hint, callbacks) {
3749 flyout.replaceChildren();
3750 const card = document.createElement("div");
3751 card.className = "desktop-mode-plugins__flyout";
3752 const hero = buildHeroSkeleton(hint);
3753 const tabs = buildTabs();
3754 const body = document.createElement("div");
3755 body.className = "desktop-mode-plugins__flyout-body";
3756 const footer = document.createElement("footer");
3757 footer.className = "desktop-mode-plugins__flyout-footer";
3758 card.append(hero.root, tabs.root, body, footer);
3759 flyout.appendChild(card);
3760 flyout.setAttribute("open", "");
3761 let info = null;
3762 const reviewsCache2 = { loaded: false };
3763 const refreshFooter = () => {
3764 paintFooter(footer, slug, info, callbacks, () => closeFlyout(flyout));
3765 };
3766 refreshFooter();
3767 tabs.onChange((tab) => {
3768 paintTabBody(body, tab, info, slug, reviewsCache2);
3769 });
3770 paintTabBody(body, "overview", info, slug, reviewsCache2);
3771 void (async () => {
3772 try {
3773 info = await fetchPluginInfo(slug);
3774 paintHero(hero, info);
3775 refreshFooter();
3776 const current = tabs.current();
3777 paintTabBody(body, current, info, slug, reviewsCache2);
3778 } catch (err) {
3779 body.innerHTML = "";
3780 const failure = document.createElement("p");
3781 failure.className = "desktop-mode-plugins__flyout-error";
3782 failure.textContent = err instanceof Error ? err.message : __("Could not load plugin details.", "desktop-mode");
3783 body.appendChild(failure);
3784 }
3785 })();
3786 }
3787 function closeFlyout(flyout) {
3788 flyout.removeAttribute("open");
3789 }
3790 function buildHeroSkeleton(hint) {
3791 const root = document.createElement("header");
3792 root.className = "desktop-mode-plugins__flyout-hero";
3793 const banner = document.createElement("div");
3794 banner.className = "desktop-mode-plugins__flyout-banner";
3795 root.appendChild(banner);
3796 const inner = document.createElement("div");
3797 inner.className = "desktop-mode-plugins__flyout-hero-inner";
3798 const icon = document.createElement("div");
3799 icon.className = "desktop-mode-plugins__flyout-hero-icon";
3800 const text = document.createElement("div");
3801 text.className = "desktop-mode-plugins__flyout-hero-text";
3802 const title = document.createElement("h2");
3803 title.className = "desktop-mode-plugins__flyout-hero-title";
3804 const byline = document.createElement("p");
3805 byline.className = "desktop-mode-plugins__flyout-hero-byline";
3806 const meta = document.createElement("div");
3807 meta.className = "desktop-mode-plugins__flyout-hero-meta";
3808 const stars = document.createElement("div");
3809 stars.className = "desktop-mode-plugins__flyout-hero-stars";
3810 meta.appendChild(stars);
3811 text.append(title, byline, meta);
3812 inner.append(icon, text);
3813 root.appendChild(inner);
3814 const close = document.createElement("button");
3815 close.type = "button";
3816 close.className = "desktop-mode-plugins__flyout-close";
3817 close.setAttribute("data-flyout-close", "");
3818 close.setAttribute(
3819 "aria-label",
3820 __("Close plugin details", "desktop-mode")
3821 );
3822 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>';
3823 root.appendChild(close);
3824 if (hint) {
3825 title.textContent = hint.name;
3826 byline.textContent = sprintf(
3827 /* translators: %s: plugin author */
3828 __("by %s", "desktop-mode"),
3829 stripHtml$2(hint.author ?? "")
3830 );
3831 const iconUrl = pickIcon(hint.icons);
3832 if (iconUrl) {
3833 const img = document.createElement("img");
3834 img.src = iconUrl;
3835 img.alt = "";
3836 icon.appendChild(img);
3837 }
3838 stars.appendChild(
3839 buildStarCluster(hint.rating ?? 0, hint.num_ratings ?? 0)
3840 );
3841 }
3842 return { root, icon, title, byline, stars, meta, banner };
3843 }
3844 function paintHero(parts, info) {
3845 parts.title.textContent = info.name;
3846 parts.byline.textContent = sprintf(
3847 /* translators: %s: plugin author */
3848 __("by %s", "desktop-mode"),
3849 stripHtml$2(info.author ?? "")
3850 );
3851 parts.icon.replaceChildren();
3852 const iconUrl = pickIcon(info.icons);
3853 if (iconUrl) {
3854 const img = document.createElement("img");
3855 img.src = iconUrl;
3856 img.alt = "";
3857 parts.icon.appendChild(img);
3858 }
3859 parts.stars.replaceChildren(
3860 buildStarCluster(info.rating ?? 0, info.num_ratings ?? 0)
3861 );
3862 const bannerUrl = info.banners?.high ?? info.banners?.low;
3863 if (bannerUrl) {
3864 parts.banner.style.backgroundImage = `url("${bannerUrl}")`;
3865 parts.banner.classList.add("has-banner");
3866 }
3867 parts.meta.querySelectorAll(":scope > .desktop-mode-plugins__flyout-meta-row").forEach((n) => n.remove());
3868 const metaRow = document.createElement("div");
3869 metaRow.className = "desktop-mode-plugins__flyout-meta-row";
3870 const installs = document.createElement("span");
3871 installs.textContent = sprintf(
3872 /* translators: %s: comma-grouped active install count */
3873 __("%s+ active", "desktop-mode"),
3874 new Intl.NumberFormat().format(info.active_installs ?? 0)
3875 );
3876 const updated = document.createElement("span");
3877 updated.textContent = sprintf(
3878 /* translators: %s: human-readable date string from wp.org */
3879 __("Updated %s", "desktop-mode"),
3880 humanDate$1(info.last_updated)
3881 );
3882 const tested = document.createElement("span");
3883 tested.textContent = info.tested ? sprintf(
3884 /* translators: %s: maximum tested WordPress version */
3885 __("Tested up to WordPress %s", "desktop-mode"),
3886 info.tested
3887 ) : "";
3888 metaRow.append(installs, updated);
3889 if (tested.textContent) {
3890 metaRow.appendChild(tested);
3891 }
3892 parts.meta.appendChild(metaRow);
3893 }
3894 function buildTabs() {
3895 const root = document.createElement("wpd-tabs");
3896 root.className = "desktop-mode-plugins__flyout-tabs";
3897 root.setAttribute("value", "overview");
3898 const labels = [
3899 { value: "overview", label: __("Overview", "desktop-mode") },
3900 { value: "screenshots", label: __("Screenshots", "desktop-mode") },
3901 { value: "reviews", label: __("Reviews", "desktop-mode") },
3902 { value: "changelog", label: __("Changelog", "desktop-mode") },
3903 { value: "faq", label: __("FAQ", "desktop-mode") }
3904 ];
3905 for (const opt of labels) {
3906 const tab = document.createElement("wpd-tab");
3907 tab.setAttribute("value", opt.value);
3908 tab.textContent = opt.label;
3909 root.appendChild(tab);
3910 }
3911 let current = "overview";
3912 const subscribers = /* @__PURE__ */ new Set();
3913 root.addEventListener("wpd-tab-change", (ev) => {
3914 const detail = ev.detail;
3915 const value = detail?.value ?? "overview";
3916 current = value;
3917 for (const cb of subscribers) {
3918 cb(current);
3919 }
3920 });
3921 return {
3922 root,
3923 current: () => current,
3924 onChange: (cb) => subscribers.add(cb)
3925 };
3926 }
3927 function paintTabBody(body, tab, info, slug, reviewsCache2) {
3928 body.replaceChildren();
3929 if (!info) {
3930 body.appendChild(buildSkeletonLines(4));
3931 return;
3932 }
3933 if (tab === "overview") {
3934 body.appendChild(buildHtmlSection(info.sections?.description ?? info.short_description ?? ""));
3935 return;
3936 }
3937 if (tab === "screenshots") {
3938 body.appendChild(buildScreenshots(info.screenshots));
3939 return;
3940 }
3941 if (tab === "changelog") {
3942 body.appendChild(buildHtmlSection(info.sections?.changelog ?? ""));
3943 return;
3944 }
3945 if (tab === "faq") {
3946 body.appendChild(buildHtmlSection(info.sections?.faq ?? ""));
3947 return;
3948 }
3949 if (tab === "reviews") {
3950 body.appendChild(buildRatingsHistogram(info));
3951 const list = document.createElement("div");
3952 list.className = "desktop-mode-plugins__reviews-list";
3953 const loadingLine = document.createElement("p");
3954 loadingLine.className = "desktop-mode-plugins__reviews-loading";
3955 loadingLine.textContent = __("Loading recent reviews…", "desktop-mode");
3956 list.appendChild(loadingLine);
3957 body.appendChild(list);
3958 if (!reviewsCache2.loaded) {
3959 void (async () => {
3960 try {
3961 const resp = await fetchPluginReviews(slug);
3962 list.replaceChildren();
3963 if (!resp.parsed || resp.items.length === 0) {
3964 const fallback = document.createElement("p");
3965 fallback.className = "desktop-mode-plugins__reviews-fallback";
3966 fallback.innerHTML = sprintf(
3967 /* translators: %s: anchor tag with link to wp.org reviews */
3968 __(
3969 "Recent reviews aren’t available right now. %s",
3970 "desktop-mode"
3971 ),
3972 `<a href="https://wordpress.org/plugins/${encodeURIComponent(slug)}/#reviews" target="_blank" rel="noopener">${__(
3973 "Read reviews on WordPress.org ↗",
3974 "desktop-mode"
3975 )}</a>`
3976 );
3977 list.appendChild(fallback);
3978 } else {
3979 for (const item of resp.items) {
3980 list.appendChild(buildReviewCard$1(item));
3981 }
3982 }
3983 reviewsCache2.loaded = true;
3984 } catch {
3985 list.replaceChildren();
3986 const failure = document.createElement("p");
3987 failure.className = "desktop-mode-plugins__reviews-fallback";
3988 failure.textContent = __(
3989 "Could not load reviews.",
3990 "desktop-mode"
3991 );
3992 list.appendChild(failure);
3993 }
3994 })();
3995 }
3996 }
3997 }
3998 function buildHtmlSection(html2) {
3999 const wrap = document.createElement("div");
4000 wrap.className = "desktop-mode-plugins__html";
4001 if (!html2) {
4002 const empty = document.createElement("p");
4003 empty.className = "desktop-mode-plugins__empty-line";
4004 empty.textContent = __("No content available.", "desktop-mode");
4005 wrap.appendChild(empty);
4006 return wrap;
4007 }
4008 wrap.innerHTML = sanitizeHtml$1(html2);
4009 wrap.querySelectorAll("a").forEach((a) => {
4010 a.setAttribute("target", "_blank");
4011 a.setAttribute("rel", "noopener nofollow");
4012 });
4013 return wrap;
4014 }
4015 function buildScreenshots(shots) {
4016 const wrap = document.createElement("div");
4017 wrap.className = "desktop-mode-plugins__screenshots";
4018 const items = shots ? Object.values(shots) : [];
4019 if (items.length === 0) {
4020 const empty = document.createElement("p");
4021 empty.className = "desktop-mode-plugins__empty-line";
4022 empty.textContent = __(
4023 "This plugin doesn’t ship screenshots.",
4024 "desktop-mode"
4025 );
4026 wrap.appendChild(empty);
4027 return wrap;
4028 }
4029 for (const shot of items) {
4030 const fig = document.createElement("figure");
4031 fig.className = "desktop-mode-plugins__screenshot";
4032 const img = document.createElement("img");
4033 img.src = shot.src;
4034 img.loading = "lazy";
4035 img.alt = shot.caption ?? "";
4036 fig.appendChild(img);
4037 if (shot.caption) {
4038 const cap = document.createElement("figcaption");
4039 cap.innerHTML = sanitizeHtml$1(shot.caption);
4040 fig.appendChild(cap);
4041 }
4042 wrap.appendChild(fig);
4043 }
4044 return wrap;
4045 }
4046 function buildRatingsHistogram(info) {
4047 const wrap = document.createElement("div");
4048 wrap.className = "desktop-mode-plugins__histogram";
4049 const ratings = info.ratings ?? {};
4050 const total = Object.values(ratings).reduce(
4051 (a, b) => a + (typeof b === "number" ? b : 0),
4052 0
4053 );
4054 if (total === 0) {
4055 const empty = document.createElement("p");
4056 empty.className = "desktop-mode-plugins__empty-line";
4057 empty.textContent = __("No ratings yet.", "desktop-mode");
4058 wrap.appendChild(empty);
4059 return wrap;
4060 }
4061 for (let star = 5; star >= 1; star--) {
4062 const count = ratings[String(star)] ?? 0;
4063 const ratio = count / total;
4064 const row = document.createElement("div");
4065 row.className = "desktop-mode-plugins__histogram-row";
4066 const label = document.createElement("span");
4067 label.className = "desktop-mode-plugins__histogram-label";
4068 label.textContent = sprintf(
4069 /* translators: %d: number of stars (1–5) */
4070 __("%d �
4071 ", "desktop-mode"),
4072 star
4073 );
4074 const track = document.createElement("span");
4075 track.className = "desktop-mode-plugins__histogram-track";
4076 const fill = document.createElement("span");
4077 fill.className = "desktop-mode-plugins__histogram-fill";
4078 fill.style.width = `${Math.round(ratio * 100)}%`;
4079 track.appendChild(fill);
4080 const num = document.createElement("span");
4081 num.className = "desktop-mode-plugins__histogram-count";
4082 num.textContent = new Intl.NumberFormat().format(count);
4083 row.append(label, track, num);
4084 wrap.appendChild(row);
4085 }
4086 return wrap;
4087 }
4088 function buildReviewCard$1(item) {
4089 const card = document.createElement("article");
4090 card.className = "desktop-mode-plugins__review";
4091 const head = document.createElement("header");
4092 head.className = "desktop-mode-plugins__review-head";
4093 const author = document.createElement("span");
4094 author.className = "desktop-mode-plugins__review-author";
4095 author.textContent = item.author || __("Anonymous", "desktop-mode");
4096 const star = buildStarCluster(item.stars / 5 * 100, 0);
4097 head.append(author, star);
4098 if (item.date) {
4099 const date = document.createElement("time");
4100 date.className = "desktop-mode-plugins__review-date";
4101 date.textContent = item.date;
4102 head.appendChild(date);
4103 }
4104 const body = document.createElement("p");
4105 body.className = "desktop-mode-plugins__review-excerpt";
4106 body.textContent = item.excerpt;
4107 card.append(head, body);
4108 if (item.url) {
4109 const link = document.createElement("a");
4110 link.href = item.url;
4111 link.target = "_blank";
4112 link.rel = "noopener nofollow";
4113 link.textContent = __("Read on WordPress.org ↗", "desktop-mode");
4114 link.className = "desktop-mode-plugins__review-link";
4115 card.appendChild(link);
4116 }
4117 return card;
4118 }
4119 function buildSkeletonLines(count) {
4120 const wrap = document.createElement("div");
4121 wrap.className = "desktop-mode-plugins__skeleton";
4122 for (let i = 0; i < count; i++) {
4123 const line = document.createElement("span");
4124 line.className = "desktop-mode-plugins__skeleton-line";
4125 line.style.width = `${60 + i * 10 % 40}%`;
4126 wrap.appendChild(line);
4127 }
4128 return wrap;
4129 }
4130 function paintFooter(footer, slug, info, callbacks, close) {
4131 footer.replaceChildren();
4132 const cfg = getConfig();
4133 const installed = callbacks.getInstalled(slug);
4134 const left = document.createElement("div");
4135 left.className = "desktop-mode-plugins__flyout-footer-left";
4136 const wpOrg = document.createElement("a");
4137 wpOrg.href = `https://wordpress.org/plugins/${encodeURIComponent(slug)}/`;
4138 wpOrg.target = "_blank";
4139 wpOrg.rel = "noopener";
4140 wpOrg.className = "desktop-mode-plugins__flyout-wporg";
4141 wpOrg.textContent = __("View on WordPress.org ↗", "desktop-mode");
4142 left.appendChild(wpOrg);
4143 const right = document.createElement("div");
4144 right.className = "desktop-mode-plugins__flyout-footer-right";
4145 if (installed) {
4146 if (cfg.caps.activate) {
4147 if (installed.status === "active" || installed.status === "active-network") {
4148 const btn = button(__("Deactivate", "desktop-mode"), "secondary");
4149 btn.addEventListener("click", () => {
4150 void doDeactivate();
4151 });
4152 right.appendChild(btn);
4153 } else {
4154 const btn = button(__("Activate", "desktop-mode"), "primary");
4155 btn.addEventListener("click", () => {
4156 void doActivate();
4157 });
4158 right.appendChild(btn);
4159 }
4160 }
4161 if (cfg.caps.delete && installed.status === "inactive") {
4162 const btn = button(__("Delete", "desktop-mode"), "danger");
4163 btn.addEventListener("click", () => {
4164 void doDelete();
4165 });
4166 right.appendChild(btn);
4167 }
4168 } else if (cfg.caps.install) {
4169 const btn = button(__("Install", "desktop-mode"), "primary");
4170 btn.addEventListener("click", () => {
4171 void doInstall(btn);
4172 });
4173 right.appendChild(btn);
4174 }
4175 footer.append(left, right);
4176 async function doInstall(btn) {
4177 const originalText = btn.textContent ?? "";
4178 btn.setAttribute("busy", "");
4179 btn.setAttribute("disabled", "");
4180 btn.textContent = __("Installing…", "desktop-mode");
4181 try {
4182 const result = await installPluginBySlug(slug);
4183 toast$3(
4184 sprintf(
4185 /* translators: %s: plugin name */
4186 __("Installed %s.", "desktop-mode"),
4187 info?.name ?? slug
4188 )
4189 );
4190 await callbacks.onPluginInstalled(result.plugin ?? "", slug);
4191 paintFooter(footer, slug, info, callbacks, close);
4192 void refreshFrameworkMenu();
4193 } catch (err) {
4194 btn.removeAttribute("busy");
4195 btn.removeAttribute("disabled");
4196 btn.textContent = originalText;
4197 toast$3(
4198 sprintf(
4199 /* translators: %s: error message */
4200 __("Install failed: %s", "desktop-mode"),
4201 describe$2(err)
4202 ),
4203 6e3
4204 );
4205 }
4206 }
4207 async function doActivate() {
4208 if (!installed) {
4209 return;
4210 }
4211 try {
4212 const updated = await activateInstalledPlugin(installed);
4213 callbacks.onPluginActivated(updated);
4214 toast$3(
4215 sprintf(
4216 /* translators: %s: plugin name */
4217 __("%s activated.", "desktop-mode"),
4218 updated.name || updated.plugin
4219 )
4220 );
4221 paintFooter(footer, slug, info, callbacks, close);
4222 void refreshFrameworkMenu();
4223 } catch (err) {
4224 toast$3(
4225 sprintf(
4226 /* translators: %s: error message */
4227 __("Activation failed: %s", "desktop-mode"),
4228 describe$2(err)
4229 ),
4230 6e3
4231 );
4232 }
4233 }
4234 async function doDeactivate() {
4235 if (!installed) {
4236 return;
4237 }
4238 try {
4239 const updated = await deactivateInstalledPlugin(installed);
4240 callbacks.onPluginDeactivated(updated);
4241 if (isDesktopModeSelf(updated.plugin)) {
4242 toast$3(
4243 __(
4244 "Desktop Mode deactivated. Reloading…",
4245 "desktop-mode"
4246 ),
4247 2e3
4248 );
4249 reloadOutOfDesktopMode();
4250 return;
4251 }
4252 toast$3(
4253 sprintf(
4254 /* translators: %s: plugin name */
4255 __("%s deactivated.", "desktop-mode"),
4256 updated.name || updated.plugin
4257 )
4258 );
4259 paintFooter(footer, slug, info, callbacks, close);
4260 void refreshFrameworkMenu();
4261 } catch (err) {
4262 toast$3(
4263 sprintf(
4264 /* translators: %s: error message */
4265 __("Deactivation failed: %s", "desktop-mode"),
4266 describe$2(err)
4267 ),
4268 6e3
4269 );
4270 }
4271 }
4272 async function doDelete() {
4273 if (!installed) {
4274 return;
4275 }
4276 const ok = await confirm$1({
4277 title: __("Delete plugin?", "desktop-mode"),
4278 message: sprintf(
4279 /* translators: %s: plugin name */
4280 __(
4281 "Permanently delete %s? Its files will be removed from disk. This cannot be undone.",
4282 "desktop-mode"
4283 ),
4284 installed.name || installed.plugin
4285 ),
4286 confirmLabel: __("Delete", "desktop-mode"),
4287 danger: true
4288 });
4289 if (!ok) {
4290 return;
4291 }
4292 try {
4293 await deleteInstalledPlugin(installed);
4294 callbacks.onPluginDeleted(installed);
4295 if (isDesktopModeSelf(installed.plugin)) {
4296 toast$3(
4297 __(
4298 "Desktop Mode deleted. Reloading…",
4299 "desktop-mode"
4300 ),
4301 2e3
4302 );
4303 reloadOutOfDesktopMode();
4304 return;
4305 }
4306 toast$3(
4307 sprintf(
4308 /* translators: %s: plugin name */
4309 __("%s deleted.", "desktop-mode"),
4310 installed.name || installed.plugin
4311 )
4312 );
4313 close();
4314 void refreshFrameworkMenu();
4315 } catch (err) {
4316 toast$3(
4317 sprintf(
4318 /* translators: %s: error message */
4319 __("Delete failed: %s", "desktop-mode"),
4320 describe$2(err)
4321 ),
4322 6e3
4323 );
4324 }
4325 }
4326 }
4327 function button(label, variant) {
4328 const b = document.createElement("wpd-button");
4329 b.setAttribute("variant", variant);
4330 b.textContent = label;
4331 return b;
4332 }
4333 function sanitizeHtml$1(html2) {
4334 const allowed = /* @__PURE__ */ new Set([
4335 "A",
4336 "ABBR",
4337 "B",
4338 "BLOCKQUOTE",
4339 "BR",
4340 "CODE",
4341 "DD",
4342 "DEL",
4343 "DIV",
4344 "DL",
4345 "DT",
4346 "EM",
4347 "FIGCAPTION",
4348 "FIGURE",
4349 "H1",
4350 "H2",
4351 "H3",
4352 "H4",
4353 "H5",
4354 "H6",
4355 "HR",
4356 "I",
4357 "IMG",
4358 "KBD",
4359 "LI",
4360 "OL",
4361 "P",
4362 "PRE",
4363 "Q",
4364 "S",
4365 "SMALL",
4366 "SPAN",
4367 "STRONG",
4368 "SUB",
4369 "SUP",
4370 "TABLE",
4371 "TBODY",
4372 "TD",
4373 "TFOOT",
4374 "TH",
4375 "THEAD",
4376 "TR",
4377 "U",
4378 "UL"
4379 ]);
4380 const allowedAttrs = /* @__PURE__ */ new Set([
4381 "href",
4382 "src",
4383 "alt",
4384 "title",
4385 "name",
4386 "rel",
4387 "target",
4388 "colspan",
4389 "rowspan"
4390 ]);
4391 const wrap = document.createElement("div");
4392 wrap.innerHTML = html2;
4393 const walker = document.createTreeWalker(wrap, NodeFilter.SHOW_ELEMENT);
4394 const toRemove = [];
4395 let current = walker.currentNode;
4396 while (current) {
4397 const next = walker.nextNode();
4398 if (current === wrap) {
4399 current = next;
4400 continue;
4401 }
4402 if (!allowed.has(current.tagName)) {
4403 toRemove.push(current);
4404 } else {
4405 for (const attr of Array.from(current.attributes)) {
4406 if (!allowedAttrs.has(attr.name.toLowerCase())) {
4407 current.removeAttribute(attr.name);
4408 }
4409 }
4410 if (current.tagName === "A") {
4411 const href = current.getAttribute("href") ?? "";
4412 if (href.startsWith("javascript:")) {
4413 current.removeAttribute("href");
4414 }
4415 }
4416 if (current.tagName === "IMG") {
4417 const src = current.getAttribute("src") ?? "";
4418 if (src.startsWith("javascript:")) {
4419 current.removeAttribute("src");
4420 }
4421 }
4422 }
4423 current = next;
4424 }
4425 for (const el of toRemove) {
4426 const text = document.createTextNode(el.textContent ?? "");
4427 el.replaceWith(text);
4428 }
4429 return wrap.innerHTML;
4430 }
4431 function describe$2(err) {
4432 if (err instanceof Error) {
4433 return err.message;
4434 }
4435 return String(err);
4436 }
4437 function stripHtml$2(html2) {
4438 const tmp = document.createElement("div");
4439 tmp.innerHTML = html2;
4440 return tmp.textContent ?? "";
4441 }
4442 function humanDate$1(raw) {
4443 if (!raw) {
4444 return "";
4445 }
4446 const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raw);
4447 if (m) {
4448 const date = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
4449 try {
4450 return date.toLocaleDateString();
4451 } catch {
4452 return raw;
4453 }
4454 }
4455 return raw;
4456 }
4457 const CANARY_TAG = "wpd-confirm-dialog";
4458 let inflight = null;
4459 function isLoaded() {
4460 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
4461 }
4462 function injectScript(scriptUrl) {
4463 return new Promise((resolve, reject) => {
4464 const existing = document.querySelector(
4465 'script[data-desktop-mode-shell-overlays="1"]'
4466 );
4467 const finish = () => {
4468 if (isLoaded()) {
4469 resolve();
4470 return;
4471 }
4472 reject(
4473 new Error(
4474 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
4475 )
4476 );
4477 };
4478 if (existing) {
4479 if (isLoaded()) {
4480 finish();
4481 } else {
4482 existing.addEventListener("load", finish);
4483 existing.addEventListener(
4484 "error",
4485 () => reject(new Error("failed to load shell-overlays bundle"))
4486 );
4487 }
4488 return;
4489 }
4490 const s = document.createElement("script");
4491 s.src = scriptUrl;
4492 s.async = true;
4493 s.dataset.desktopModeShellOverlays = "1";
4494 s.addEventListener("load", finish);
4495 s.addEventListener(
4496 "error",
4497 () => reject(new Error("failed to load shell-overlays bundle"))
4498 );
4499 document.head.appendChild(s);
4500 });
4501 }
4502 function ensureShellOverlaysLoaded(scriptUrl) {
4503 if (isLoaded()) {
4504 return Promise.resolve();
4505 }
4506 if (!scriptUrl) {
4507 return Promise.resolve();
4508 }
4509 if (!inflight) {
4510 inflight = injectScript(scriptUrl);
4511 }
4512 return inflight;
4513 }
4514 function shellOverlaysBundleUrl() {
4515 const cfg = window.desktopModeConfig;
4516 return cfg?.shellOverlaysBundleUrl ?? "";
4517 }
4518 function openWithShellOverlays(isStillCurrent, fn) {
4519 const url = shellOverlaysBundleUrl();
4520 if (isLoaded() || !url) {
4521 fn();
4522 return;
4523 }
4524 void ensureShellOverlaysLoaded(url).then(() => {
4525 if (!isStillCurrent()) {
4526 return;
4527 }
4528 fn();
4529 }).catch((err) => {
4530 if (typeof console !== "undefined") {
4531 console.warn(
4532 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
4533 err
4534 );
4535 }
4536 });
4537 }
4538 async function wpdConfirm(options) {
4539 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
4540 return new Promise((resolve) => {
4541 const dialog = document.createElement("wpd-confirm-dialog");
4542 dialog.setAttribute("open", "");
4543 if (options.title) {
4544 dialog.setAttribute("title", options.title);
4545 }
4546 dialog.setAttribute("message", options.message);
4547 if (options.confirmLabel) {
4548 dialog.setAttribute("confirm-label", options.confirmLabel);
4549 }
4550 if (options.cancelLabel) {
4551 dialog.setAttribute("cancel-label", options.cancelLabel);
4552 }
4553 {
4554 dialog.setAttribute("danger", "");
4555 }
4556 if (options.hideCancel) {
4557 dialog.setAttribute("hide-cancel", "");
4558 }
4559 if (options.dismissable) {
4560 dialog.setAttribute("dismissable", "");
4561 }
4562 const cleanup = (ok) => {
4563 dialog.remove();
4564 resolve(ok);
4565 };
4566 dialog.addEventListener("wpd-confirm", () => cleanup(true));
4567 dialog.addEventListener("wpd-cancel", () => cleanup(false));
4568 document.body.appendChild(dialog);
4569 const inner = dialog.shadowRoot?.querySelector(".dialog");
4570 (inner ?? dialog).focus?.();
4571 });
4572 }
4573 const DEFAULT_DURATION_MS = 4e3;
4574 const FADE_OUT_MS = 200;
4575 function showToast(options) {
4576 const intent = activity.filter(
4577 "desktop-mode/toast-requested",
4578 { ...options }
4579 );
4580 if (!intent || intent.cancel === true) {
4581 return () => void 0;
4582 }
4583 let dismissRequested = false;
4584 let realDismiss = null;
4585 openWithShellOverlays(
4586 () => !dismissRequested,
4587 () => {
4588 realDismiss = renderToast(intent);
4589 }
4590 );
4591 return () => {
4592 dismissRequested = true;
4593 if (realDismiss) {
4594 realDismiss();
4595 }
4596 };
4597 }
4598 function renderToast(intent) {
4599 const container = ensureContainer();
4600 const toast2 = document.createElement("wpd-toast");
4601 toast2.textContent = intent.message;
4602 if (intent.action) {
4603 toast2.setAttribute("action", intent.action.label);
4604 toast2.addEventListener("wpd-toast-action", () => {
4605 intent.action?.onClick();
4606 dismiss();
4607 });
4608 }
4609 container.appendChild(toast2);
4610 let dismissed = false;
4611 let dismissTimer = null;
4612 const dismiss = () => {
4613 if (dismissed) {
4614 return;
4615 }
4616 dismissed = true;
4617 if (dismissTimer !== null) {
4618 window.clearTimeout(dismissTimer);
4619 dismissTimer = null;
4620 }
4621 toast2.setAttribute("state", "out");
4622 window.setTimeout(() => {
4623 toast2.remove();
4624 }, FADE_OUT_MS);
4625 };
4626 requestAnimationFrame(() => {
4627 toast2.setAttribute("state", "in");
4628 });
4629 dismissTimer = window.setTimeout(
4630 dismiss,
4631 intent.duration ?? DEFAULT_DURATION_MS
4632 );
4633 activity.publish("desktop-mode/toast-shown", { ...intent });
4634 return dismiss;
4635 }
4636 function ensureContainer() {
4637 const existing = document.querySelector(
4638 "wpd-toast-container"
4639 );
4640 if (existing) {
4641 return existing;
4642 }
4643 const el = document.createElement("wpd-toast-container");
4644 document.body.appendChild(el);
4645 return el;
4646 }
4647 const PLUGINS_CHANGED_TOPIC$3 = "desktop-mode.plugin.changed";
4648 const PLUGINS_CHANGED_SOURCE = "upload-dialog";
4649 function openUploadDialog(host, prefilled, callbacks = {}) {
4650 return new Promise((resolve) => {
4651 const overlay = document.createElement("div");
4652 overlay.className = "desktop-mode-plugins__upload-overlay";
4653 const card = document.createElement("div");
4654 card.className = "desktop-mode-plugins__upload-card";
4655 card.setAttribute("role", "dialog");
4656 card.setAttribute("aria-modal", "true");
4657 card.setAttribute(
4658 "aria-label",
4659 __("Upload a plugin .zip", "desktop-mode")
4660 );
4661 const heading = document.createElement("h2");
4662 heading.className = "desktop-mode-plugins__upload-heading";
4663 heading.textContent = __("Upload a plugin", "desktop-mode");
4664 const lede = document.createElement("p");
4665 lede.className = "desktop-mode-plugins__upload-lede";
4666 lede.textContent = __(
4667 "Pick a .zip file from your computer, or drop one onto the area below.",
4668 "desktop-mode"
4669 );
4670 const dropZone = document.createElement("div");
4671 dropZone.className = "desktop-mode-plugins__upload-dropzone";
4672 dropZone.tabIndex = 0;
4673 dropZone.setAttribute("role", "button");
4674 dropZone.setAttribute(
4675 "aria-label",
4676 __(
4677 "Drop a .zip plugin file here, or click to choose a file.",
4678 "desktop-mode"
4679 )
4680 );
4681 const dropIcon = document.createElement("span");
4682 dropIcon.className = "dashicons dashicons-upload desktop-mode-plugins__upload-icon";
4683 dropIcon.setAttribute("aria-hidden", "true");
4684 const dropHint = document.createElement("p");
4685 dropHint.className = "desktop-mode-plugins__upload-hint";
4686 dropHint.textContent = __(
4687 "Drop your .zip here or click to browse",
4688 "desktop-mode"
4689 );
4690 const fileLabel = document.createElement("p");
4691 fileLabel.className = "desktop-mode-plugins__upload-filename";
4692 fileLabel.hidden = true;
4693 dropZone.append(dropIcon, dropHint, fileLabel);
4694 const input = document.createElement("input");
4695 input.type = "file";
4696 input.accept = ".zip,application/zip,application/x-zip-compressed";
4697 input.style.display = "none";
4698 dropZone.appendChild(input);
4699 const status = document.createElement("p");
4700 status.className = "desktop-mode-plugins__upload-status";
4701 status.hidden = true;
4702 const actions = document.createElement("div");
4703 actions.className = "desktop-mode-plugins__upload-actions";
4704 const cancelBtn = document.createElement("wpd-button");
4705 cancelBtn.setAttribute("variant", "ghost");
4706 cancelBtn.textContent = __("Cancel", "desktop-mode");
4707 const submitBtn = document.createElement("wpd-button");
4708 submitBtn.setAttribute("variant", "primary");
4709 submitBtn.textContent = __("Install", "desktop-mode");
4710 submitBtn.setAttribute("disabled", "");
4711 actions.append(cancelBtn, submitBtn);
4712 card.append(heading, lede, dropZone, status, actions);
4713 overlay.appendChild(card);
4714 host.appendChild(overlay);
4715 const swallowDrag = (ev) => {
4716 ev.preventDefault();
4717 ev.stopPropagation();
4718 };
4719 overlay.addEventListener("dragenter", swallowDrag);
4720 overlay.addEventListener("dragover", swallowDrag);
4721 overlay.addEventListener("drop", swallowDrag);
4722 let pickedFile = null;
4723 let uploading = false;
4724 const setFile = (file) => {
4725 pickedFile = file;
4726 if (file) {
4727 dropZone.classList.add("has-file");
4728 fileLabel.hidden = false;
4729 fileLabel.textContent = sprintf(
4730 /* translators: 1: file name, 2: file size in KB */
4731 __("%1$s · %2$s KB", "desktop-mode"),
4732 file.name,
4733 Math.round(file.size / 1024).toString()
4734 );
4735 submitBtn.removeAttribute("disabled");
4736 } else {
4737 dropZone.classList.remove("has-file");
4738 fileLabel.hidden = true;
4739 submitBtn.setAttribute("disabled", "");
4740 }
4741 };
4742 dropZone.addEventListener("click", (ev) => {
4743 if (ev.target?.tagName === "INPUT") {
4744 return;
4745 }
4746 input.click();
4747 });
4748 dropZone.addEventListener("keydown", (ev) => {
4749 if (ev.key === "Enter" || ev.key === " ") {
4750 ev.preventDefault();
4751 input.click();
4752 }
4753 });
4754 dropZone.addEventListener("dragover", (ev) => {
4755 ev.preventDefault();
4756 ev.stopPropagation();
4757 dropZone.classList.add("is-hovered");
4758 });
4759 dropZone.addEventListener("dragleave", (ev) => {
4760 ev.stopPropagation();
4761 dropZone.classList.remove("is-hovered");
4762 });
4763 dropZone.addEventListener("drop", (ev) => {
4764 ev.preventDefault();
4765 ev.stopPropagation();
4766 dropZone.classList.remove("is-hovered");
4767 const file = ev.dataTransfer?.files?.[0];
4768 if (file && isZip(file)) {
4769 setFile(file);
4770 } else if (file) {
4771 showStatus(
4772 __("Only .zip files are accepted.", "desktop-mode"),
4773 "error"
4774 );
4775 }
4776 });
4777 input.addEventListener("change", () => {
4778 const file = input.files?.[0];
4779 if (file && isZip(file)) {
4780 setFile(file);
4781 }
4782 });
4783 const close = (result) => {
4784 document.removeEventListener("keydown", onKey);
4785 overlay.remove();
4786 resolve(result);
4787 };
4788 const onKey = (ev) => {
4789 if (ev.key === "Escape" && !uploading) {
4790 close(null);
4791 }
4792 };
4793 document.addEventListener("keydown", onKey);
4794 cancelBtn.addEventListener("click", () => {
4795 if (uploading) {
4796 return;
4797 }
4798 close(null);
4799 });
4800 submitBtn.addEventListener("click", () => {
4801 if (!pickedFile || uploading) {
4802 return;
4803 }
4804 void runUpload();
4805 });
4806 overlay.addEventListener("click", (ev) => {
4807 if (ev.target === overlay && !uploading) {
4808 close(null);
4809 }
4810 });
4811 if (prefilled && isZip(prefilled)) {
4812 setFile(prefilled);
4813 }
4814 async function runUpload(overwrite = false) {
4815 if (!pickedFile) {
4816 return;
4817 }
4818 uploading = true;
4819 submitBtn.setAttribute("busy", "");
4820 submitBtn.setAttribute("disabled", "");
4821 cancelBtn.setAttribute("disabled", "");
4822 showStatus(
4823 overwrite ? __("Replacing existing plugin…", "desktop-mode") : __("Uploading and installing…", "desktop-mode"),
4824 "info"
4825 );
4826 try {
4827 const result = await uploadPluginZip(pickedFile, { overwrite });
4828 if (callbacks.onUploaded) {
4829 callbacks.onUploaded(result);
4830 }
4831 broadcast(PLUGINS_CHANGED_TOPIC$3, {
4832 source: PLUGINS_CHANGED_SOURCE,
4833 plugin: result.plugin_file,
4834 action: "install"
4835 });
4836 void refreshFrameworkMenu();
4837 showSuccessPanel(result);
4838 } catch (err) {
4839 const errStatus = err.status;
4840 const errCode = err.code;
4841 if (!overwrite && (errStatus === 409 || errCode === "folder_exists")) {
4842 uploading = false;
4843 submitBtn.removeAttribute("busy");
4844 submitBtn.removeAttribute("disabled");
4845 cancelBtn.removeAttribute("disabled");
4846 showStatus(
4847 __(
4848 "A plugin with the same folder name is already installed.",
4849 "desktop-mode"
4850 ),
4851 "info"
4852 );
4853 const ok = await wpdConfirm({
4854 title: __("Replace existing plugin?", "desktop-mode"),
4855 message: __(
4856 "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.",
4857 "desktop-mode"
4858 ),
4859 confirmLabel: __("Replace", "desktop-mode"),
4860 cancelLabel: __("Cancel", "desktop-mode")
4861 });
4862 if (ok) {
4863 await runUpload(true);
4864 }
4865 return;
4866 }
4867 uploading = false;
4868 submitBtn.removeAttribute("busy");
4869 submitBtn.removeAttribute("disabled");
4870 cancelBtn.removeAttribute("disabled");
4871 const message = err instanceof Error ? err.message : String(err);
4872 showStatus(
4873 sprintf(
4874 /* translators: %s: error message from the upload handler */
4875 __("Upload failed: %s", "desktop-mode"),
4876 message
4877 ),
4878 "error"
4879 );
4880 }
4881 }
4882 function showSuccessPanel(result) {
4883 uploading = false;
4884 dropZone.remove();
4885 input.remove();
4886 actions.remove();
4887 status.hidden = true;
4888 const successHeading = document.createElement("h3");
4889 successHeading.className = "desktop-mode-plugins__upload-success-heading";
4890 successHeading.textContent = __(
4891 "Plugin installed successfully.",
4892 "desktop-mode"
4893 );
4894 const detail = document.createElement("p");
4895 detail.className = "desktop-mode-plugins__upload-success-detail";
4896 const name = result.plugin_name || result.plugin_file;
4897 detail.textContent = result.plugin_version ? sprintf(
4898 /* translators: 1: plugin name 2: plugin version */
4899 __("%1$s %2$s", "desktop-mode"),
4900 name,
4901 result.plugin_version
4902 ) : name;
4903 const successActions = document.createElement("div");
4904 successActions.className = "desktop-mode-plugins__upload-actions";
4905 const closeBtn = document.createElement("wpd-button");
4906 closeBtn.setAttribute("variant", "ghost");
4907 closeBtn.textContent = __("Close", "desktop-mode");
4908 const activateBtn = document.createElement("wpd-button");
4909 activateBtn.setAttribute("variant", "primary");
4910 activateBtn.textContent = __("Activate Plugin", "desktop-mode");
4911 successActions.append(closeBtn, activateBtn);
4912 card.append(successHeading, detail, successActions);
4913 closeBtn.addEventListener("click", () => {
4914 if (uploading) {
4915 return;
4916 }
4917 close(result);
4918 });
4919 activateBtn.addEventListener("click", () => {
4920 if (uploading) {
4921 return;
4922 }
4923 void runActivate();
4924 });
4925 async function runActivate() {
4926 uploading = true;
4927 activateBtn.setAttribute("busy", "");
4928 activateBtn.setAttribute("disabled", "");
4929 closeBtn.setAttribute("disabled", "");
4930 try {
4931 const pluginFile = result.plugin_file.endsWith(".php") ? result.plugin_file.slice(0, -4) : result.plugin_file;
4932 const updated = await activateInstalledPlugin({
4933 plugin: pluginFile,
4934 status: "inactive"
4935 });
4936 if (callbacks.onActivated) {
4937 callbacks.onActivated(result.plugin_file);
4938 }
4939 void refreshFrameworkMenu();
4940 broadcast(PLUGINS_CHANGED_TOPIC$3, {
4941 source: PLUGINS_CHANGED_SOURCE,
4942 plugin: updated.plugin,
4943 action: "activate"
4944 });
4945 showToast({
4946 message: sprintf(
4947 /* translators: %s: plugin name */
4948 __("%s activated.", "desktop-mode"),
4949 name
4950 )
4951 });
4952 uploading = false;
4953 successHeading.textContent = __(
4954 "Plugin activated.",
4955 "desktop-mode"
4956 );
4957 activateBtn.remove();
4958 closeBtn.removeAttribute("disabled");
4959 closeBtn.setAttribute("variant", "primary");
4960 closeBtn.textContent = __("Done", "desktop-mode");
4961 closeBtn.focus?.();
4962 } catch (err) {
4963 uploading = false;
4964 activateBtn.removeAttribute("busy");
4965 activateBtn.removeAttribute("disabled");
4966 closeBtn.removeAttribute("disabled");
4967 const message = err instanceof Error ? err.message : String(err);
4968 status.hidden = false;
4969 status.dataset.tone = "error";
4970 status.textContent = sprintf(
4971 /* translators: %s: error message from the activate handler */
4972 __("Activate failed: %s", "desktop-mode"),
4973 message
4974 );
4975 card.appendChild(status);
4976 }
4977 }
4978 window.setTimeout(() => activateBtn.focus?.(), 16);
4979 }
4980 function showStatus(message, tone) {
4981 status.hidden = false;
4982 status.dataset.tone = tone;
4983 status.textContent = message;
4984 }
4985 window.setTimeout(() => dropZone.focus(), 16);
4986 });
4987 }
4988 function isZip(file) {
4989 if (file.size <= 0) {
4990 return false;
4991 }
4992 const name = file.name.toLowerCase();
4993 if (name.endsWith(".zip")) {
4994 return true;
4995 }
4996 return file.type === "application/zip" || file.type === "application/x-zip-compressed";
4997 }
4998 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}`;
4999 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}`;
5000 const _WpdSegment = class _WpdSegment extends Component {
5001 render() {
5002 this.setAttribute("role", "radio");
5003 return html`
5004 <button type="button" @click=${() => this._onPick()}>
5005 <slot></slot>
5006 </button>
5007 `;
5008 }
5009 _onPick() {
5010 this.emit("wpd-segment-pick", {
5011 value: this.value
5012 });
5013 }
5014 };
5015 _WpdSegment.props = ["value"];
5016 _WpdSegment.styles = [segmentStyles];
5017 _WpdSegment.help = {
5018 title: "Segment",
5019 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
5020 status: "stable",
5021 since: "0.9.0",
5022 props: [
5023 {
5024 name: "value",
5025 type: "string",
5026 description: "Identifier this segment contributes to the parent group selection."
5027 }
5028 ],
5029 slots: [
5030 { name: "(default)", description: "Visible segment label." }
5031 ],
5032 events: [
5033 {
5034 name: "wpd-segment-pick",
5035 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
5036 detail: "{ value: string }"
5037 }
5038 ]
5039 };
5040 let WpdSegment = _WpdSegment;
5041 defineComponent("wpd-segment", WpdSegment);
5042 const _WpdSegmented = class _WpdSegmented extends Component {
5043 connectedCallback() {
5044 super.connectedCallback();
5045 this.addEventListener("wpd-segment-pick", (e) => {
5046 const detail = e.detail;
5047 e.stopPropagation();
5048 this.value = detail.value;
5049 this.emit("wpd-pick", { value: detail.value });
5050 });
5051 }
5052 /**
5053 * Declarative item-list setter. Replaces the existing
5054 * `<wpd-segment>` children with a fresh set built from a
5055 * `{ value, label }` array; preserves the current selection
5056 * when the value still matches an entry, otherwise falls back
5057 * to the first item.
5058 *
5059 * Collapses the pre-0.11 imperative dance (clear children,
5060 * `createElement`, set `textContent`, `appendChild`, then
5061 * `setAttribute('value', …)` on the group — order matters) to
5062 * a single assignment:
5063 *
5064 * ```js
5065 * segmented.items = [
5066 * { value: 'm', label: 'm' },
5067 * { value: 'km', label: 'km' },
5068 * ];
5069 * ```
5070 *
5071 * @since 0.11.0
5072 */
5073 set items(list) {
5074 const existing = this.querySelectorAll(":scope > wpd-segment");
5075 for (const el of Array.from(existing)) {
5076 el.remove();
5077 }
5078 for (const item of list) {
5079 const seg = document.createElement("wpd-segment");
5080 seg.setAttribute("value", item.value);
5081 seg.textContent = item.label;
5082 this.appendChild(seg);
5083 }
5084 const current = this.value;
5085 const stillValid = current !== null && list.some((i) => i.value === current);
5086 if (!stillValid && list.length > 0) {
5087 this.value = list[0].value;
5088 } else {
5089 this.requestUpdate();
5090 }
5091 }
5092 render() {
5093 const label = this.label || "";
5094 if (label) {
5095 this.setAttribute("aria-label", label);
5096 }
5097 this.setAttribute("role", "radiogroup");
5098 const current = this.value;
5099 queueMicrotask(() => {
5100 const segs = this.querySelectorAll("wpd-segment");
5101 for (const seg of Array.from(segs)) {
5102 const v = seg.getAttribute("value");
5103 seg.setAttribute(
5104 "aria-checked",
5105 v === current ? "true" : "false"
5106 );
5107 }
5108 });
5109 return html`<slot></slot>`;
5110 }
5111 };
5112 _WpdSegmented.props = ["value", "label"];
5113 _WpdSegmented.styles = [segmentedStyles];
5114 _WpdSegmented.help = {
5115 title: "Segmented",
5116 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
5117 status: "stable",
5118 since: "0.9.0",
5119 props: [
5120 {
5121 name: "value",
5122 type: "string",
5123 description: "Currently selected segment value. Mirrored onto child aria-checked."
5124 },
5125 {
5126 name: "label",
5127 type: "string",
5128 description: "aria-label for the radiogroup."
5129 }
5130 ],
5131 slots: [
5132 { name: "(default)", description: '<wpd-segment value="…"> children.' }
5133 ],
5134 events: [
5135 {
5136 name: "wpd-pick",
5137 description: "Fires when the selected segment changes.",
5138 detail: "{ value: string }"
5139 }
5140 ],
5141 cssProps: [
5142 { name: "--desktop-mode-window-bg", description: "Pill background." },
5143 { name: "--desktop-mode-text", description: "Active label colour." },
5144 { name: "--desktop-mode-muted", description: "Inactive label colour." }
5145 ],
5146 example: html`
5147 <wpd-segmented value="md" label="Dock size">
5148 <wpd-segment value="sm">Small</wpd-segment>
5149 <wpd-segment value="md">Medium</wpd-segment>
5150 <wpd-segment value="lg">Large</wpd-segment>
5151 </wpd-segmented>
5152 `
5153 };
5154 let WpdSegmented = _WpdSegmented;
5155 defineComponent("wpd-segmented", WpdSegmented);
5156 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}`;
5157 const _WpdTextField = class _WpdTextField extends Component {
5158 constructor() {
5159 super(...arguments);
5160 this._revealed = false;
5161 }
5162 connectedCallback() {
5163 super.connectedCallback();
5164 ensureAutoId(this);
5165 }
5166 render() {
5167 const label = this.label || "";
5168 const value = this.value ?? "";
5169 const placeholder = this.placeholder || "";
5170 const disabled = this.disabled !== null;
5171 const readonly = this.readonly !== null;
5172 const declaredAutocomplete = this.autocomplete;
5173 const declaredType = this.type || "text";
5174 const isPassword = declaredType === "password";
5175 let autocomplete = declaredAutocomplete || "off";
5176 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
5177 autocomplete = "new-password";
5178 }
5179 const maxLength = this.maxlength;
5180 const minLength = this.minlength;
5181 const pattern = this.pattern || "";
5182 const name = this.name || "";
5183 const suffix = this.suffix || "";
5184 const invalid = this.invalid !== null;
5185 const reveal = this.reveal !== null;
5186 const isPasswordIntent = declaredType === "password";
5187 const isMasked = isPasswordIntent && !(reveal && this._revealed);
5188 let effectiveType;
5189 if (isPasswordIntent) {
5190 effectiveType = "text";
5191 } else if (reveal && this._revealed) {
5192 effectiveType = "text";
5193 } else {
5194 effectiveType = declaredType;
5195 }
5196 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
5197 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
5198 const hostId = this.id || "wpd-unnamed";
5199 const inputId = `${hostId}__input`;
5200 return html`
5201 ${label ? html`<label
5202 class="wpd-text-field__label"
5203 for=${inputId}
5204 >${label}</label>` : html``}
5205 <span class=${rowClass}>
5206 <input
5207 id=${inputId}
5208 class=${inputClass}
5209 type=${effectiveType}
5210 .value=${value}
5211 placeholder=${placeholder}
5212 ?disabled=${disabled}
5213 ?readonly=${readonly}
5214 autocomplete=${autocomplete}
5215 maxlength=${maxLength ?? ""}
5216 minlength=${minLength ?? ""}
5217 pattern=${pattern}
5218 name=${name}
5219 aria-invalid=${invalid ? "true" : "false"}
5220 aria-label=${label || ""}
5221 @input=${(e) => this._onInput(e)}
5222 @change=${(e) => this._onChange(e)}
5223 @keydown=${(e) => this._onKeyDown(e)}
5224 />
5225 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
5226 ${reveal ? this._renderRevealButton(disabled) : html``}
5227 </span>
5228 `;
5229 }
5230 _renderRevealButton(disabled) {
5231 const label = this._revealed ? "Hide" : "Show";
5232 return html`
5233 <button
5234 type="button"
5235 class="wpd-text-field__reveal"
5236 aria-label=${label}
5237 aria-pressed=${this._revealed ? "true" : "false"}
5238 ?disabled=${disabled}
5239 tabindex="0"
5240 @click=${() => this._onToggleReveal()}
5241 >
5242 ${this._revealed ? _iconEyeOff() : _iconEye()}
5243 </button>
5244 `;
5245 }
5246 _onToggleReveal() {
5247 this._revealed = !this._revealed;
5248 this.requestUpdate();
5249 }
5250 _onInput(e) {
5251 const input = e.target;
5252 this.value = input.value;
5253 this.emit("wpd-input-change", { value: input.value });
5254 }
5255 _onChange(e) {
5256 const input = e.target;
5257 this.emit("wpd-input-commit", { value: input.value });
5258 }
5259 _onKeyDown(e) {
5260 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
5261 const input = e.target;
5262 this.emit("wpd-submit", { value: input.value });
5263 }
5264 }
5265 };
5266 _WpdTextField.props = [
5267 "label",
5268 "value",
5269 "placeholder",
5270 "disabled",
5271 "readonly",
5272 "autocomplete",
5273 "type",
5274 "maxlength",
5275 "minlength",
5276 "pattern",
5277 "name",
5278 "suffix",
5279 "invalid",
5280 "reveal"
5281 ];
5282 _WpdTextField.styles = [textFieldStyles];
5283 _WpdTextField.help = {
5284 title: "Text field",
5285 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.",
5286 status: "stable",
5287 since: "0.11.0",
5288 props: [
5289 { name: "label", type: "string", description: "Visible label above the input." },
5290 { name: "value", type: "string", description: "Current input value; reflected two-way." },
5291 { name: "placeholder", type: "string", description: "Native placeholder string." },
5292 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
5293 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
5294 {
5295 name: "autocomplete",
5296 type: "string",
5297 default: "off",
5298 description: "Forwarded to the native input autocomplete attribute."
5299 },
5300 {
5301 name: "type",
5302 type: "string",
5303 default: "text",
5304 description: "Native input type (text, password, email, search, tel, url)."
5305 },
5306 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
5307 { name: "minlength", type: "integer (string)", description: "Native minlength." },
5308 { name: "pattern", type: "regex string", description: "Native validation pattern." },
5309 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
5310 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
5311 {
5312 name: "invalid",
5313 type: "boolean attribute",
5314 description: "Marks the field aria-invalid and applies the error style."
5315 },
5316 {
5317 name: "reveal",
5318 type: "boolean attribute",
5319 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
5320 }
5321 ],
5322 events: [
5323 {
5324 name: "wpd-input-change",
5325 description: "Fires on every input keystroke.",
5326 detail: "{ value: string }"
5327 },
5328 {
5329 name: "wpd-input-commit",
5330 description: "Fires on the native change event (blur / Enter).",
5331 detail: "{ value: string }"
5332 },
5333 {
5334 name: "wpd-submit",
5335 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
5336 detail: "{ value: string }"
5337 }
5338 ],
5339 cssProps: [
5340 { name: "--desktop-mode-text", description: "Text colour." },
5341 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
5342 { name: "--desktop-mode-border", description: "Input outline." },
5343 { name: "--desktop-mode-window-bg", description: "Input background." }
5344 ],
5345 example: html`
5346 <wpd-stack gap="8">
5347 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
5348 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
5349 </wpd-stack>
5350 `
5351 };
5352 let WpdTextField = _WpdTextField;
5353 defineComponent("wpd-text-field", WpdTextField);
5354 function _iconEye() {
5355 return html`
5356 <svg
5357 viewBox="0 0 16 16"
5358 width="14"
5359 height="14"
5360 fill="none"
5361 stroke="currentColor"
5362 stroke-width="1.5"
5363 stroke-linecap="round"
5364 stroke-linejoin="round"
5365 aria-hidden="true"
5366 focusable="false"
5367 >
5368 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
5369 <circle cx="8" cy="8" r="2" />
5370 </svg>
5371 `;
5372 }
5373 function _iconEyeOff() {
5374 return html`
5375 <svg
5376 viewBox="0 0 16 16"
5377 width="14"
5378 height="14"
5379 fill="none"
5380 stroke="currentColor"
5381 stroke-width="1.5"
5382 stroke-linecap="round"
5383 stroke-linejoin="round"
5384 aria-hidden="true"
5385 focusable="false"
5386 >
5387 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
5388 <circle cx="8" cy="8" r="2" />
5389 <line x1="2" y1="2" x2="14" y2="14" />
5390 </svg>
5391 `;
5392 }
5393 const PLUGINS_CHANGED_TOPIC$2 = "desktop-mode.plugin.changed";
5394 const SOURCE$2 = "browse-view";
5395 function toast$2(message, duration = 3500) {
5396 const api2 = window.wp?.desktop;
5397 if (api2 && typeof api2.showToast === "function") {
5398 api2.showToast({ message, duration });
5399 return;
5400 }
5401 console.log("[plugins-window]", message);
5402 }
5403 function mountBrowseView(host, flyoutEl, bodyEl) {
5404 host.replaceChildren();
5405 const state = {
5406 filter: "featured",
5407 search: "",
5408 page: 1,
5409 totalPages: 0,
5410 loading: false,
5411 exhausted: false,
5412 plugins: [],
5413 installed: /* @__PURE__ */ new Map(),
5414 cardsBySlug: /* @__PURE__ */ new Map()
5415 };
5416 const toolbar = document.createElement("header");
5417 toolbar.className = "desktop-mode-plugins__toolbar";
5418 const left = document.createElement("div");
5419 left.className = "desktop-mode-plugins__toolbar-left";
5420 const segmented = document.createElement("wpd-segmented");
5421 segmented.setAttribute("value", "featured");
5422 const filters = [
5423 { value: "featured", label: __("Featured", "desktop-mode") },
5424 { value: "popular", label: __("Popular", "desktop-mode") },
5425 { value: "recommended", label: __("Recommended", "desktop-mode") },
5426 { value: "favorites", label: __("Favorites", "desktop-mode") },
5427 { value: "new", label: __("New", "desktop-mode") },
5428 { value: "beta", label: __("Beta", "desktop-mode") }
5429 ];
5430 for (const opt of filters) {
5431 const seg = document.createElement("wpd-segment");
5432 seg.setAttribute("value", opt.value);
5433 seg.textContent = opt.label;
5434 segmented.appendChild(seg);
5435 }
5436 segmented.addEventListener("wpd-pick", (ev) => {
5437 const next = ev.detail?.value ?? "featured";
5438 state.filter = next;
5439 void resetAndLoad();
5440 });
5441 const search = document.createElement("wpd-text-field");
5442 search.setAttribute("placeholder", __("Search WordPress.org…", "desktop-mode"));
5443 let searchDebounce;
5444 search.addEventListener("wpd-input-change", (ev) => {
5445 const value = ev.detail?.value ?? "";
5446 window.clearTimeout(searchDebounce);
5447 searchDebounce = window.setTimeout(() => {
5448 state.search = value;
5449 void resetAndLoad();
5450 }, 250);
5451 });
5452 left.append(segmented, search);
5453 const right = document.createElement("div");
5454 right.className = "desktop-mode-plugins__toolbar-trailing";
5455 const cfg = getConfig();
5456 if (cfg.caps.upload) {
5457 const upload = document.createElement("wpd-button");
5458 upload.setAttribute("variant", "secondary");
5459 upload.innerHTML = '<span class="dashicons dashicons-upload" aria-hidden="true"></span> ' + __("Upload Plugin", "desktop-mode");
5460 upload.addEventListener("click", () => {
5461 void openUploadDialog(bodyEl, null, {
5462 onUploaded: () => void refreshInstalled()
5463 });
5464 });
5465 right.appendChild(upload);
5466 }
5467 const refreshButton = document.createElement("wpd-button");
5468 refreshButton.setAttribute("variant", "ghost");
5469 refreshButton.setAttribute("title", __("Refresh", "desktop-mode"));
5470 refreshButton.innerHTML = '<span class="dashicons dashicons-update" aria-hidden="true"></span>';
5471 refreshButton.addEventListener("click", () => {
5472 void refreshInstalled();
5473 void resetAndLoad();
5474 });
5475 right.appendChild(refreshButton);
5476 toolbar.append(left, right);
5477 const gallery = document.createElement("div");
5478 gallery.className = "desktop-mode-plugins__gallery";
5479 const sentinel = document.createElement("div");
5480 sentinel.className = "desktop-mode-plugins__gallery-sentinel";
5481 sentinel.setAttribute("aria-hidden", "true");
5482 const status = document.createElement("p");
5483 status.className = "desktop-mode-plugins__gallery-status";
5484 status.hidden = true;
5485 host.append(toolbar, gallery, status);
5486 const dropOverlay = document.createElement("div");
5487 dropOverlay.className = "desktop-mode-plugins__window-drop";
5488 dropOverlay.setAttribute("aria-hidden", "true");
5489 const dropMsg = document.createElement("p");
5490 dropMsg.textContent = __(
5491 "Drop the .zip to install.",
5492 "desktop-mode"
5493 );
5494 dropOverlay.appendChild(dropMsg);
5495 bodyEl.appendChild(dropOverlay);
5496 let dragDepth = 0;
5497 const isZipDrag = (ev) => Boolean(
5498 ev.dataTransfer?.types.includes("Files")
5499 );
5500 const onDragEnter = (ev) => {
5501 if (!cfg.caps.upload) {
5502 return;
5503 }
5504 if (!isZipDrag(ev)) {
5505 return;
5506 }
5507 dragDepth++;
5508 bodyEl.classList.add("has-zip-dragover");
5509 };
5510 const onDragLeave = (ev) => {
5511 if (!cfg.caps.upload || !isZipDrag(ev)) {
5512 return;
5513 }
5514 dragDepth = Math.max(0, dragDepth - 1);
5515 if (dragDepth === 0) {
5516 bodyEl.classList.remove("has-zip-dragover");
5517 }
5518 };
5519 const onDragOver = (ev) => {
5520 if (cfg.caps.upload && isZipDrag(ev)) {
5521 ev.preventDefault();
5522 }
5523 };
5524 const onDrop = (ev) => {
5525 if (!cfg.caps.upload) {
5526 return;
5527 }
5528 const file = ev.dataTransfer?.files?.[0];
5529 dragDepth = 0;
5530 bodyEl.classList.remove("has-zip-dragover");
5531 if (!file) {
5532 return;
5533 }
5534 ev.preventDefault();
5535 void openUploadDialog(bodyEl, file, {
5536 onUploaded: () => void refreshInstalled()
5537 });
5538 };
5539 bodyEl.addEventListener("dragenter", onDragEnter);
5540 bodyEl.addEventListener("dragleave", onDragLeave);
5541 bodyEl.addEventListener("dragover", onDragOver);
5542 bodyEl.addEventListener("drop", onDrop);
5543 const teardownDropTargets = installPluginDropTargets();
5544 const cardCallbacks = {
5545 onOpen: (slug, hint) => {
5546 if (!flyoutEl) {
5547 return;
5548 }
5549 openDetailFlyout(flyoutEl, slug, hint, {
5550 getInstalled: (s) => state.installed.get(s),
5551 onPluginInstalled: async (pluginFile, slug2) => {
5552 await refreshInstalled();
5553 const card = state.cardsBySlug.get(slug2);
5554 const plugin = state.plugins.find((p) => p.slug === slug2);
5555 if (card && plugin) {
5556 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5557 }
5558 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5559 source: SOURCE$2,
5560 plugin: pluginFile ?? slug2,
5561 action: "install"
5562 });
5563 if (pluginFile) {
5564 console.log("[plugins-window] installed", pluginFile);
5565 }
5566 },
5567 onPluginActivated: (updated) => {
5568 state.installed.set(indexKeyFor$1(updated), updated);
5569 const card = state.cardsBySlug.get(updated.textdomain ?? "");
5570 const plugin = state.plugins.find(
5571 (p) => p.slug === (updated.textdomain ?? "")
5572 );
5573 if (card && plugin) {
5574 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5575 }
5576 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5577 source: SOURCE$2,
5578 plugin: updated.plugin,
5579 action: "activate"
5580 });
5581 },
5582 onPluginDeactivated: (updated) => {
5583 state.installed.set(indexKeyFor$1(updated), updated);
5584 const card = state.cardsBySlug.get(updated.textdomain ?? "");
5585 const plugin = state.plugins.find(
5586 (p) => p.slug === (updated.textdomain ?? "")
5587 );
5588 if (card && plugin) {
5589 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5590 }
5591 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5592 source: SOURCE$2,
5593 plugin: updated.plugin,
5594 action: "deactivate"
5595 });
5596 },
5597 onPluginDeleted: (deleted) => {
5598 const key = indexKeyFor$1(deleted);
5599 state.installed.delete(key);
5600 const card = state.cardsBySlug.get(deleted.textdomain ?? "");
5601 const plugin = state.plugins.find(
5602 (p) => p.slug === (deleted.textdomain ?? "")
5603 );
5604 if (card && plugin) {
5605 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5606 }
5607 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5608 source: SOURCE$2,
5609 plugin: deleted.plugin,
5610 action: "delete"
5611 });
5612 }
5613 });
5614 },
5615 onInstall: async (plugin, card) => {
5616 const cta = card.querySelector("[data-plugin-card-cta]");
5617 const ctaOriginalText = cta?.textContent ?? "";
5618 cta?.setAttribute("busy", "");
5619 cta?.setAttribute("disabled", "");
5620 if (cta) {
5621 cta.textContent = __("Installing…", "desktop-mode");
5622 }
5623 try {
5624 await installPluginBySlug(plugin.slug);
5625 await refreshInstalled();
5626 toast$2(
5627 sprintf(
5628 /* translators: %s: plugin name */
5629 __("Installed %s.", "desktop-mode"),
5630 plugin.name
5631 )
5632 );
5633 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5634 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5635 source: SOURCE$2,
5636 plugin: plugin.slug,
5637 action: "install"
5638 });
5639 void refreshFrameworkMenu();
5640 } catch (err) {
5641 cta?.removeAttribute("busy");
5642 cta?.removeAttribute("disabled");
5643 if (cta) {
5644 cta.textContent = ctaOriginalText;
5645 }
5646 toast$2(
5647 sprintf(
5648 /* translators: %s: error message */
5649 __("Install failed: %s", "desktop-mode"),
5650 describe$1(err)
5651 ),
5652 6e3
5653 );
5654 }
5655 },
5656 onActivate: async (installed, card) => {
5657 const cta = card.querySelector("[data-plugin-card-cta]");
5658 const ctaOriginalText = cta?.textContent ?? "";
5659 cta?.setAttribute("busy", "");
5660 cta?.setAttribute("disabled", "");
5661 if (cta) {
5662 cta.textContent = __("Activating…", "desktop-mode");
5663 }
5664 try {
5665 const updated = await activateInstalledPlugin(installed);
5666 state.installed.set(indexKeyFor$1(updated), updated);
5667 toast$2(
5668 sprintf(
5669 /* translators: %s: plugin name */
5670 __("%s activated.", "desktop-mode"),
5671 updated.name || updated.plugin
5672 )
5673 );
5674 const plugin = state.plugins.find(
5675 (p) => p.slug === (updated.textdomain ?? "")
5676 );
5677 if (plugin) {
5678 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5679 }
5680 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5681 source: SOURCE$2,
5682 plugin: updated.plugin,
5683 action: "activate"
5684 });
5685 void refreshFrameworkMenu();
5686 } catch (err) {
5687 cta?.removeAttribute("busy");
5688 cta?.removeAttribute("disabled");
5689 if (cta) {
5690 cta.textContent = ctaOriginalText;
5691 }
5692 toast$2(
5693 sprintf(
5694 /* translators: %s: error message */
5695 __("Activation failed: %s", "desktop-mode"),
5696 describe$1(err)
5697 ),
5698 6e3
5699 );
5700 }
5701 }
5702 };
5703 const observer = new IntersectionObserver(
5704 (entries) => {
5705 for (const entry of entries) {
5706 if (entry.isIntersecting) {
5707 void loadMore();
5708 }
5709 }
5710 },
5711 { root: gallery, rootMargin: "240px", threshold: 0 }
5712 );
5713 observer.observe(sentinel);
5714 void refreshInstalled();
5715 void resetAndLoad();
5716 async function refreshInstalled() {
5717 try {
5718 const rows = await fetchInstalledPlugins();
5719 state.installed = new Map(
5720 rows.map((r) => [indexKeyFor$1(r), r])
5721 );
5722 for (const [slug, card] of state.cardsBySlug) {
5723 const plugin = state.plugins.find((p) => p.slug === slug);
5724 if (plugin) {
5725 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5726 }
5727 }
5728 } catch {
5729 }
5730 }
5731 async function resetAndLoad() {
5732 state.page = 1;
5733 state.totalPages = 0;
5734 state.exhausted = false;
5735 state.plugins = [];
5736 state.cardsBySlug.clear();
5737 gallery.replaceChildren();
5738 for (let i = 0; i < 6; i++) {
5739 gallery.appendChild(buildSkeletonCard$1());
5740 }
5741 gallery.appendChild(sentinel);
5742 await loadMore();
5743 }
5744 const inflightSkeletons = [];
5745 function showInflightLoader() {
5746 if (inflightSkeletons.length > 0) {
5747 return;
5748 }
5749 for (let i = 0; i < 4; i++) {
5750 const skel = buildSkeletonCard$1();
5751 gallery.insertBefore(skel, sentinel);
5752 inflightSkeletons.push(skel);
5753 }
5754 }
5755 function clearInflightLoader() {
5756 for (const skel of inflightSkeletons) {
5757 skel.remove();
5758 }
5759 inflightSkeletons.length = 0;
5760 }
5761 async function loadMore() {
5762 if (state.loading || state.exhausted) {
5763 return;
5764 }
5765 state.loading = true;
5766 if (state.page > 1) {
5767 showInflightLoader();
5768 }
5769 try {
5770 const data = await browsePlugins({
5771 browse: state.search === "" ? state.filter : void 0,
5772 search: state.search === "" ? void 0 : state.search,
5773 page: state.page,
5774 perPage: 24
5775 });
5776 if (state.page === 1) {
5777 gallery.replaceChildren();
5778 gallery.appendChild(sentinel);
5779 }
5780 const info = data.info ?? {};
5781 if (typeof info.pages === "number" && info.pages > 0) {
5782 state.totalPages = info.pages;
5783 }
5784 const incoming = data.plugins ?? [];
5785 if (incoming.length === 0) {
5786 state.exhausted = true;
5787 if (state.page === 1) {
5788 showStatus(__("No plugins matched.", "desktop-mode"));
5789 }
5790 return;
5791 }
5792 for (const plugin of incoming) {
5793 if (!plugin?.slug) {
5794 continue;
5795 }
5796 if (state.cardsBySlug.has(plugin.slug)) {
5797 continue;
5798 }
5799 const card = buildCard(plugin, state.installed, cardCallbacks);
5800 makeCardDraggable(card, plugin);
5801 gallery.insertBefore(card, sentinel);
5802 state.cardsBySlug.set(plugin.slug, card);
5803 state.plugins.push(plugin);
5804 }
5805 state.page++;
5806 if (state.totalPages > 0 && state.page > state.totalPages) {
5807 state.exhausted = true;
5808 } else if (state.totalPages === 0 && incoming.length < 24) {
5809 state.exhausted = true;
5810 }
5811 hideStatus();
5812 } catch (err) {
5813 showStatus(
5814 sprintf(
5815 /* translators: %s: error message */
5816 __("Could not load plugins: %s", "desktop-mode"),
5817 describe$1(err)
5818 )
5819 );
5820 } finally {
5821 clearInflightLoader();
5822 state.loading = false;
5823 }
5824 }
5825 function showStatus(message) {
5826 status.hidden = false;
5827 status.textContent = message;
5828 }
5829 function hideStatus() {
5830 status.hidden = true;
5831 status.textContent = "";
5832 }
5833 const unsubscribePluginsChanged = subscribe(
5834 PLUGINS_CHANGED_TOPIC$2,
5835 (payload) => {
5836 if (payload?.source === SOURCE$2) {
5837 return;
5838 }
5839 void refreshInstalled();
5840 }
5841 );
5842 return () => {
5843 unsubscribePluginsChanged();
5844 observer.disconnect();
5845 bodyEl.removeEventListener("dragenter", onDragEnter);
5846 bodyEl.removeEventListener("dragleave", onDragLeave);
5847 bodyEl.removeEventListener("dragover", onDragOver);
5848 bodyEl.removeEventListener("drop", onDrop);
5849 dropOverlay.remove();
5850 teardownDropTargets();
5851 host.replaceChildren();
5852 };
5853 }
5854 function buildSkeletonCard$1() {
5855 const card = document.createElement("wpd-card");
5856 card.classList.add(
5857 "desktop-mode-plugins__card",
5858 "desktop-mode-plugins__card--skeleton"
5859 );
5860 card.setAttribute("aria-hidden", "true");
5861 for (let i = 0; i < 4; i++) {
5862 const line = document.createElement("span");
5863 line.className = "desktop-mode-plugins__skeleton-line";
5864 line.style.width = `${50 + i * 17 % 50}%`;
5865 card.appendChild(line);
5866 }
5867 return card;
5868 }
5869 function indexKeyFor$1(plugin) {
5870 return plugin.textdomain || plugin.plugin;
5871 }
5872 function describe$1(err) {
5873 if (err instanceof Error) {
5874 return err.message;
5875 }
5876 return String(err);
5877 }
5878 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 )}`;
5879 const _WpdRibbon = class _WpdRibbon extends Component {
5880 render() {
5881 return html`<span class="banner" part="banner"><slot></slot></span>`;
5882 }
5883 };
5884 _WpdRibbon.props = ["placement", "tone"];
5885 _WpdRibbon.styles = [styles$8];
5886 _WpdRibbon.help = {
5887 title: "Ribbon",
5888 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.",
5889 status: "experimental",
5890 since: "0.20.0",
5891 props: [
5892 {
5893 name: "placement",
5894 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
5895 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
5896 },
5897 {
5898 name: "tone",
5899 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
5900 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
5901 }
5902 ],
5903 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
5904 cssProps: [
5905 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
5906 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
5907 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
5908 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
5909 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
5910 { name: "--wpd-ribbon-fg", default: "#fff" },
5911 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
5912 { name: "--wpd-ribbon-padding", default: "4px 0" },
5913 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
5914 { name: "--wpd-ribbon-tracking", default: "0.06em" },
5915 { name: "--wpd-ribbon-z", default: "2" }
5916 ],
5917 example: html`
5918 <div
5919 style="position: relative; width: 240px; height: 120px;
5920 border: 1px solid #ccc; border-radius: 8px;
5921 padding: 16px; box-sizing: border-box;"
5922 >
5923 <wpd-ribbon>Featured</wpd-ribbon>
5924 Card body…
5925 </div>
5926 `
5927 };
5928 let WpdRibbon = _WpdRibbon;
5929 defineComponent("wpd-ribbon", WpdRibbon);
5930 const PLUGINS_CHANGED_TOPIC$1 = "desktop-mode.plugin.changed";
5931 const SOURCE$1 = "featured-view";
5932 function toast$1(message, duration = 3500) {
5933 const api2 = window.wp?.desktop;
5934 if (api2 && typeof api2.showToast === "function") {
5935 api2.showToast({ message, duration });
5936 return;
5937 }
5938 console.log("[plugins-window]", message);
5939 }
5940 function mountFeaturedView(host, flyoutEl) {
5941 host.replaceChildren();
5942 const state = {
5943 plugins: [],
5944 installed: /* @__PURE__ */ new Map(),
5945 cardsBySlug: /* @__PURE__ */ new Map(),
5946 loading: true
5947 };
5948 const intro = document.createElement("header");
5949 intro.className = "desktop-mode-plugins__featured-intro";
5950 const heading = document.createElement("h2");
5951 heading.className = "desktop-mode-plugins__featured-heading";
5952 heading.textContent = __("Made for Desktop Mode", "desktop-mode");
5953 const description = document.createElement("p");
5954 description.className = "desktop-mode-plugins__featured-blurb";
5955 description.textContent = __(
5956 "Plugins that extend Desktop Mode — desktop decorations, native windows, widgets, and other companions.",
5957 "desktop-mode"
5958 );
5959 intro.append(heading, description);
5960 const gallery = document.createElement("div");
5961 gallery.className = "desktop-mode-plugins__gallery";
5962 const status = document.createElement("p");
5963 status.className = "desktop-mode-plugins__gallery-status";
5964 status.hidden = true;
5965 host.append(intro, gallery, status);
5966 const cardCallbacks = {
5967 onOpen: (slug, hint) => {
5968 if (!flyoutEl) {
5969 return;
5970 }
5971 openDetailFlyout(flyoutEl, slug, hint, {
5972 getInstalled: (s) => state.installed.get(s),
5973 onPluginInstalled: async (pluginFile, slug2) => {
5974 await refreshInstalled();
5975 repaintSlugCard(slug2);
5976 broadcast(PLUGINS_CHANGED_TOPIC$1, {
5977 source: SOURCE$1,
5978 plugin: pluginFile ?? slug2,
5979 action: "install"
5980 });
5981 },
5982 onPluginActivated: (updated) => {
5983 state.installed.set(indexKeyFor(updated), updated);
5984 repaintSlugCard(updated.textdomain ?? "");
5985 broadcast(PLUGINS_CHANGED_TOPIC$1, {
5986 source: SOURCE$1,
5987 plugin: updated.plugin,
5988 action: "activate"
5989 });
5990 },
5991 onPluginDeactivated: (updated) => {
5992 state.installed.set(indexKeyFor(updated), updated);
5993 repaintSlugCard(updated.textdomain ?? "");
5994 broadcast(PLUGINS_CHANGED_TOPIC$1, {
5995 source: SOURCE$1,
5996 plugin: updated.plugin,
5997 action: "deactivate"
5998 });
5999 },
6000 onPluginDeleted: (deleted) => {
6001 state.installed.delete(indexKeyFor(deleted));
6002 repaintSlugCard(deleted.textdomain ?? "");
6003 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6004 source: SOURCE$1,
6005 plugin: deleted.plugin,
6006 action: "delete"
6007 });
6008 }
6009 });
6010 },
6011 onInstall: async (plugin, card) => {
6012 const cta = card.querySelector("[data-plugin-card-cta]");
6013 const originalText = cta?.textContent ?? "";
6014 cta?.setAttribute("busy", "");
6015 cta?.setAttribute("disabled", "");
6016 if (cta) {
6017 cta.textContent = __("Installing…", "desktop-mode");
6018 }
6019 try {
6020 await installPluginBySlug(plugin.slug);
6021 await refreshInstalled();
6022 toast$1(
6023 sprintf(
6024 /* translators: %s: plugin name */
6025 __("Installed %s.", "desktop-mode"),
6026 plugin.name
6027 )
6028 );
6029 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6030 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6031 source: SOURCE$1,
6032 plugin: plugin.slug,
6033 action: "install"
6034 });
6035 void refreshFrameworkMenu();
6036 } catch (err) {
6037 cta?.removeAttribute("busy");
6038 cta?.removeAttribute("disabled");
6039 if (cta) {
6040 cta.textContent = originalText;
6041 }
6042 toast$1(
6043 sprintf(
6044 /* translators: %s: error message */
6045 __("Install failed: %s", "desktop-mode"),
6046 formatError(err)
6047 ),
6048 6e3
6049 );
6050 }
6051 },
6052 onActivate: async (installed, card) => {
6053 const cta = card.querySelector("[data-plugin-card-cta]");
6054 const originalText = cta?.textContent ?? "";
6055 cta?.setAttribute("busy", "");
6056 cta?.setAttribute("disabled", "");
6057 if (cta) {
6058 cta.textContent = __("Activating…", "desktop-mode");
6059 }
6060 try {
6061 const updated = await activateInstalledPlugin(installed);
6062 state.installed.set(indexKeyFor(updated), updated);
6063 toast$1(
6064 sprintf(
6065 /* translators: %s: plugin name */
6066 __("%s activated.", "desktop-mode"),
6067 updated.name || updated.plugin
6068 )
6069 );
6070 const plugin = state.plugins.find(
6071 (p) => p.slug === (updated.textdomain ?? "")
6072 );
6073 if (plugin) {
6074 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6075 }
6076 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6077 source: SOURCE$1,
6078 plugin: updated.plugin,
6079 action: "activate"
6080 });
6081 void refreshFrameworkMenu();
6082 } catch (err) {
6083 cta?.removeAttribute("busy");
6084 cta?.removeAttribute("disabled");
6085 if (cta) {
6086 cta.textContent = originalText;
6087 }
6088 toast$1(
6089 sprintf(
6090 /* translators: %s: error message */
6091 __("Activation failed: %s", "desktop-mode"),
6092 formatError(err)
6093 ),
6094 6e3
6095 );
6096 }
6097 }
6098 };
6099 void load();
6100 async function load() {
6101 state.loading = true;
6102 paintSkeletons();
6103 try {
6104 const [featured, installed] = await Promise.all([
6105 fetchFeaturedPlugins(),
6106 fetchInstalledPlugins().catch(() => [])
6107 ]);
6108 state.installed = new Map(
6109 installed.map((r) => [indexKeyFor(r), r])
6110 );
6111 state.plugins = featured.plugins ?? [];
6112 renderGallery();
6113 if (state.plugins.length === 0) {
6114 showStatus(__("No featured plugins yet.", "desktop-mode"));
6115 } else {
6116 hideStatus();
6117 }
6118 } catch (err) {
6119 gallery.replaceChildren();
6120 showStatus(
6121 sprintf(
6122 /* translators: %s: error message */
6123 __("Could not load featured plugins: %s", "desktop-mode"),
6124 formatError(err)
6125 )
6126 );
6127 } finally {
6128 state.loading = false;
6129 }
6130 }
6131 function paintSkeletons() {
6132 gallery.replaceChildren();
6133 state.cardsBySlug.clear();
6134 for (let i = 0; i < 3; i++) {
6135 gallery.appendChild(buildSkeletonCard());
6136 }
6137 }
6138 function renderGallery() {
6139 gallery.replaceChildren();
6140 state.cardsBySlug.clear();
6141 for (const plugin of state.plugins) {
6142 if (!plugin?.slug) {
6143 continue;
6144 }
6145 const card = buildCard(plugin, state.installed, cardCallbacks);
6146 if (plugin.featured) {
6147 card.classList.add("desktop-mode-plugins__card--featured");
6148 const ribbon = document.createElement("wpd-ribbon");
6149 ribbon.textContent = __("Featured", "desktop-mode");
6150 card.prepend(ribbon);
6151 }
6152 makeCardDraggable(card, plugin);
6153 gallery.appendChild(card);
6154 state.cardsBySlug.set(plugin.slug, card);
6155 }
6156 }
6157 function repaintSlugCard(slug) {
6158 if (!slug) {
6159 return;
6160 }
6161 const card = state.cardsBySlug.get(slug);
6162 const plugin = state.plugins.find((p) => p.slug === slug);
6163 if (card && plugin) {
6164 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6165 }
6166 }
6167 async function refreshInstalled() {
6168 try {
6169 const rows = await fetchInstalledPlugins();
6170 state.installed = new Map(
6171 rows.map((r) => [indexKeyFor(r), r])
6172 );
6173 for (const [slug, card] of state.cardsBySlug) {
6174 const plugin = state.plugins.find((p) => p.slug === slug);
6175 if (plugin) {
6176 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6177 }
6178 }
6179 } catch {
6180 }
6181 }
6182 function showStatus(message) {
6183 status.hidden = false;
6184 status.textContent = message;
6185 }
6186 function hideStatus() {
6187 status.hidden = true;
6188 status.textContent = "";
6189 }
6190 const unsubscribePluginsChanged = subscribe(
6191 PLUGINS_CHANGED_TOPIC$1,
6192 (payload) => {
6193 if (payload?.source === SOURCE$1) {
6194 return;
6195 }
6196 void refreshInstalled();
6197 }
6198 );
6199 return () => {
6200 unsubscribePluginsChanged();
6201 host.replaceChildren();
6202 };
6203 }
6204 function buildSkeletonCard() {
6205 const card = document.createElement("wpd-card");
6206 card.classList.add(
6207 "desktop-mode-plugins__card",
6208 "desktop-mode-plugins__card--skeleton"
6209 );
6210 card.setAttribute("aria-hidden", "true");
6211 for (let i = 0; i < 4; i++) {
6212 const line = document.createElement("span");
6213 line.className = "desktop-mode-plugins__skeleton-line";
6214 line.style.width = `${50 + i * 17 % 50}%`;
6215 card.appendChild(line);
6216 }
6217 return card;
6218 }
6219 function indexKeyFor(plugin) {
6220 return plugin.textdomain || plugin.plugin;
6221 }
6222 function formatError(err) {
6223 if (err instanceof Error) {
6224 return err.message;
6225 }
6226 return String(err);
6227 }
6228 const queue = [];
6229 let inFlight = false;
6230 function enqueueUpdateJob(run) {
6231 return new Promise((resolve, reject) => {
6232 queue.push({
6233 run,
6234 resolve,
6235 reject
6236 });
6237 void drain();
6238 });
6239 }
6240 async function drain() {
6241 if (inFlight) {
6242 return;
6243 }
6244 const job = queue.shift();
6245 if (!job) {
6246 return;
6247 }
6248 inFlight = true;
6249 try {
6250 const value = await job.run();
6251 job.resolve(value);
6252 } catch (err) {
6253 job.reject(err);
6254 } finally {
6255 inFlight = false;
6256 void Promise.resolve().then(drain);
6257 }
6258 }
6259 const WP_ORG_ASSET_RE = /^(https:\/\/ps\.w\.org\/[a-z0-9-]+\/assets\/)icon\.svg$/i;
6260 function buildCandidates(initialUrl) {
6261 const match = initialUrl.match(WP_ORG_ASSET_RE);
6262 if (!match) {
6263 return [initialUrl];
6264 }
6265 const base = match[1];
6266 return [
6267 initialUrl,
6268 base + "icon-256x256.png",
6269 base + "icon-256x256.gif",
6270 base + "icon-128x128.png",
6271 base + "icon-128x128.gif"
6272 ];
6273 }
6274 function attachIconFallback(img, initialUrl, onExhausted) {
6275 const candidates = buildCandidates(initialUrl);
6276 let index = 0;
6277 img.addEventListener("error", () => {
6278 index += 1;
6279 if (index < candidates.length) {
6280 img.src = candidates[index];
6281 return;
6282 }
6283 onExhausted();
6284 });
6285 return candidates[0];
6286 }
6287 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 )}`;
6288 const _WpdChip = class _WpdChip extends Component {
6289 constructor() {
6290 super(...arguments);
6291 this._onHostKeyDown = (e) => {
6292 const dismissible = this.dismissible !== null;
6293 if (!dismissible) {
6294 return;
6295 }
6296 if (e.key === "Backspace" || e.key === "Delete") {
6297 e.preventDefault();
6298 const disabled = this.disabled !== null;
6299 if (disabled) {
6300 return;
6301 }
6302 const label = this.label ?? "";
6303 this.emit("wpd-chip-dismiss", { label });
6304 }
6305 };
6306 }
6307 connectedCallback() {
6308 super.connectedCallback();
6309 this.addEventListener("keydown", this._onHostKeyDown);
6310 }
6311 disconnectedCallback() {
6312 this.removeEventListener("keydown", this._onHostKeyDown);
6313 }
6314 render() {
6315 const label = this.label ?? "";
6316 const dismissible = this.dismissible !== null;
6317 const disabled = this.disabled !== null;
6318 return html`
6319 <span part="chip" class="wpd-chip">
6320 <span class="wpd-chip__icon">
6321 <slot name="icon"></slot>
6322 </span>
6323 <span class="wpd-chip__label">
6324 ${label === "" ? html`<slot></slot>` : label}
6325 </span>
6326 ${dismissible ? html`
6327 <button
6328 part="dismiss"
6329 class="wpd-chip__dismiss"
6330 type="button"
6331 aria-label=${`Remove ${label || "chip"}`}
6332 ?disabled=${disabled}
6333 @click=${(e) => this._onDismiss(e)}
6334 >
6335 ${_iconCross()}
6336 </button>
6337 ` : html``}
6338 </span>
6339 `;
6340 }
6341 _onDismiss(e) {
6342 e.stopPropagation();
6343 const disabled = this.disabled !== null;
6344 if (disabled) {
6345 return;
6346 }
6347 const label = this.label ?? "";
6348 this.emit("wpd-chip-dismiss", { label });
6349 }
6350 };
6351 _WpdChip.props = [
6352 "label",
6353 "tone",
6354 "size",
6355 "dismissible",
6356 "disabled",
6357 "pending"
6358 ];
6359 _WpdChip.styles = [styles$7];
6360 _WpdChip.help = {
6361 title: "Chip",
6362 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.",
6363 status: "experimental",
6364 since: "0.8.0",
6365 props: [
6366 {
6367 name: "label",
6368 type: "string",
6369 description: "Visible text. Falls back to the default slot when omitted."
6370 },
6371 {
6372 name: "tone",
6373 type: "'neutral' | 'accent' | 'positive' | 'warning' | 'danger'",
6374 default: "neutral",
6375 description: "Color variant. Mirrors <wpd-badge> tones."
6376 },
6377 {
6378 name: "size",
6379 type: "'default' | 'compact'",
6380 default: "default",
6381 description: "Vertical density. Compact halves horizontal padding for dense lists."
6382 },
6383 {
6384 name: "dismissible",
6385 type: "boolean attribute",
6386 description: "Renders a trailing × button. Click / Enter / Space emits wpd-chip-dismiss."
6387 },
6388 {
6389 name: "disabled",
6390 type: "boolean attribute",
6391 description: "Visually mutes the chip and blocks the dismiss button. Useful while a parent is mid-update."
6392 },
6393 {
6394 name: "pending",
6395 type: "boolean attribute",
6396 description: "Renders a subtle pulse animation while a REST mutation is in flight. Auto-applied by <wpd-tag-input>; safe to set by hand."
6397 }
6398 ],
6399 slots: [
6400 { name: "(default)", description: "Fallback label when `label` is unset." },
6401 {
6402 name: "icon",
6403 description: "Leading icon (Dashicon, SVG, image). Inherits text color."
6404 }
6405 ],
6406 parts: [
6407 { name: "chip", description: "The pill container." },
6408 {
6409 name: "dismiss",
6410 description: "The trailing × button (when `dismissible`)."
6411 }
6412 ],
6413 events: [
6414 {
6415 name: "wpd-chip-dismiss",
6416 description: "Fires when the dismiss button is activated. Detail carries the chip's label so a delegated listener can act without DOM walking.",
6417 detail: "{ label: string }"
6418 }
6419 ],
6420 cssProps: [
6421 { name: "--wpd-chip-bg", description: "Background color." },
6422 { name: "--wpd-chip-fg", description: "Text color." },
6423 { name: "--wpd-chip-border", description: "Border shorthand." },
6424 {
6425 name: "--wpd-chip-padding",
6426 description: "Padding shorthand.",
6427 default: "2px 8px"
6428 },
6429 {
6430 name: "--wpd-chip-radius",
6431 description: "Corner radius.",
6432 default: "999px"
6433 },
6434 {
6435 name: "--wpd-chip-label-max",
6436 description: "Max width of the inner label before ellipsis.",
6437 default: "220px"
6438 }
6439 ],
6440 example: html`
6441 <wpd-cluster gap="6">
6442 <wpd-chip label="Neutral"></wpd-chip>
6443 <wpd-chip label="Accent" tone="accent"></wpd-chip>
6444 <wpd-chip label="Positive" tone="positive"></wpd-chip>
6445 <wpd-chip label="Warning" tone="warning"></wpd-chip>
6446 <wpd-chip label="Danger" tone="danger"></wpd-chip>
6447 <wpd-chip label="Dismissible" dismissible></wpd-chip>
6448 </wpd-cluster>
6449 `
6450 };
6451 let WpdChip = _WpdChip;
6452 defineComponent("wpd-chip", WpdChip);
6453 function _iconCross() {
6454 return html`
6455 <svg
6456 viewBox="0 0 12 12"
6457 width="10"
6458 height="10"
6459 aria-hidden="true"
6460 focusable="false"
6461 fill="none"
6462 stroke="currentColor"
6463 stroke-width="1.5"
6464 stroke-linecap="round"
6465 >
6466 <path d="M3 3 L9 9 M9 3 L3 9" />
6467 </svg>
6468 `;
6469 }
6470 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}`;
6471 const _WpdCluster = class _WpdCluster extends Component {
6472 render() {
6473 const gap = this.gap;
6474 const justify = this.justify;
6475 const align = this.align;
6476 const gapPx = gap && /^\d+$/.test(gap) ? `${gap}px` : "";
6477 if (gapPx) {
6478 this.style.setProperty("--wpd-cluster-gap", gapPx);
6479 }
6480 if (justify) {
6481 this.style.setProperty("--wpd-cluster-justify", justify);
6482 }
6483 if (align) {
6484 this.style.setProperty("--wpd-cluster-align", align);
6485 }
6486 return html`<slot></slot>`;
6487 }
6488 };
6489 _WpdCluster.props = ["gap", "justify", "align"];
6490 _WpdCluster.styles = [styles$6];
6491 _WpdCluster.help = {
6492 title: "Cluster",
6493 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.",
6494 status: "stable",
6495 since: "0.10.0",
6496 props: [
6497 {
6498 name: "gap",
6499 type: "integer (px)",
6500 default: "8",
6501 description: "Space between children."
6502 },
6503 {
6504 name: "justify",
6505 type: "'start' | 'center' | 'end' | 'space-between' | 'space-around'",
6506 default: "start",
6507 description: "Main-axis alignment (justify-content)."
6508 },
6509 {
6510 name: "align",
6511 type: "'start' | 'center' | 'end' | 'stretch' | 'baseline'",
6512 default: "center",
6513 description: "Cross-axis alignment (align-items)."
6514 }
6515 ],
6516 slots: [
6517 { name: "(default)", description: "Inline children." }
6518 ],
6519 cssProps: [
6520 { name: "--wpd-cluster-gap", default: "8px" },
6521 { name: "--wpd-cluster-justify", default: "start" },
6522 { name: "--wpd-cluster-align", default: "center" }
6523 ],
6524 example: html`
6525 <wpd-cluster gap="8" justify="end">
6526 <wpd-button>Cancel</wpd-button>
6527 <wpd-button variant="primary">Save</wpd-button>
6528 </wpd-cluster>
6529 `
6530 };
6531 let WpdCluster = _WpdCluster;
6532 defineComponent("wpd-cluster", WpdCluster);
6533 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}`;
6534 const _WpdStack = class _WpdStack extends Component {
6535 render() {
6536 const gap = this.gap;
6537 const align = this.align;
6538 const padding = this.padding;
6539 const gapPx = gap && /^\d+$/.test(gap) ? `${gap}px` : "";
6540 if (gapPx) {
6541 this.style.setProperty("--wpd-stack-gap", gapPx);
6542 }
6543 if (align) {
6544 this.style.setProperty("--wpd-stack-align", align);
6545 }
6546 if (padding !== null && /^\d+$/.test(padding)) {
6547 this.style.setProperty("--wpd-stack-padding", `${padding}px`);
6548 }
6549 return html`<slot></slot>`;
6550 }
6551 };
6552 _WpdStack.props = ["gap", "align", "padding"];
6553 _WpdStack.styles = [styles$5];
6554 _WpdStack.help = {
6555 title: "Stack",
6556 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.',
6557 status: "stable",
6558 since: "0.10.0",
6559 props: [
6560 {
6561 name: "gap",
6562 type: "integer (px)",
6563 default: "12",
6564 description: "Space between children."
6565 },
6566 {
6567 name: "align",
6568 type: "'start' | 'center' | 'end' | 'stretch'",
6569 default: "stretch",
6570 description: "Cross-axis alignment (align-items)."
6571 },
6572 {
6573 name: "padding",
6574 type: "integer (px)",
6575 default: "0",
6576 description: "Inset padding on every side. Pass 0 for edge-to-edge."
6577 }
6578 ],
6579 slots: [
6580 { name: "(default)", description: "Stacked children." }
6581 ],
6582 cssProps: [
6583 { name: "--wpd-stack-gap", default: "12px" },
6584 { name: "--wpd-stack-align", default: "stretch" },
6585 { name: "--wpd-stack-padding", default: "0" }
6586 ],
6587 example: html`
6588 <wpd-stack gap="12">
6589 <wpd-section heading="Foo">First</wpd-section>
6590 <wpd-section heading="Bar">Second</wpd-section>
6591 </wpd-stack>
6592 `
6593 };
6594 let WpdStack = _WpdStack;
6595 defineComponent("wpd-stack", WpdStack);
6596 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}`;
6597 const _WpdGrid = class _WpdGrid extends Component {
6598 render() {
6599 const columns = this.columns;
6600 const rows = this.rows;
6601 const gap = this.gap;
6602 const cg = this["column-gap"];
6603 const rg = this["row-gap"];
6604 if (columns && /^\d+$/.test(columns)) {
6605 this.style.setProperty(
6606 "--wpd-grid-columns",
6607 `repeat(${columns}, minmax(0, 1fr))`
6608 );
6609 }
6610 if (rows && /^\d+$/.test(rows)) {
6611 this.style.setProperty(
6612 "--wpd-grid-rows",
6613 `repeat(${rows}, minmax(0, 1fr))`
6614 );
6615 }
6616 if (gap && /^\d+$/.test(gap)) {
6617 this.style.setProperty("--wpd-grid-gap", `${gap}px`);
6618 }
6619 if (cg && /^\d+$/.test(cg)) {
6620 this.style.setProperty("--wpd-grid-column-gap", `${cg}px`);
6621 }
6622 if (rg && /^\d+$/.test(rg)) {
6623 this.style.setProperty("--wpd-grid-row-gap", `${rg}px`);
6624 }
6625 return html`<slot></slot>`;
6626 }
6627 };
6628 _WpdGrid.props = ["columns", "rows", "gap", "column-gap", "row-gap"];
6629 _WpdGrid.styles = [styles$4];
6630 _WpdGrid.help = {
6631 title: "Grid",
6632 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.',
6633 status: "stable",
6634 since: "0.10.0",
6635 props: [
6636 {
6637 name: "columns",
6638 type: "integer",
6639 default: "1",
6640 description: "Number of equal-width columns (repeat(N, minmax(0, 1fr)))."
6641 },
6642 {
6643 name: "rows",
6644 type: "integer",
6645 description: "Optional fixed row count. Omit for content-driven sizing."
6646 },
6647 { name: "gap", type: "integer (px)", description: "Cell spacing on both axes." },
6648 { name: "column-gap", type: "integer (px)", description: "x-axis override." },
6649 { name: "row-gap", type: "integer (px)", description: "y-axis override." }
6650 ],
6651 slots: [
6652 { name: "(default)", description: "Grid children." }
6653 ],
6654 cssProps: [
6655 { name: "--wpd-grid-columns" },
6656 { name: "--wpd-grid-rows" },
6657 { name: "--wpd-grid-gap" },
6658 { name: "--wpd-grid-column-gap" },
6659 { name: "--wpd-grid-row-gap" }
6660 ],
6661 example: html`
6662 <wpd-grid columns="4" gap="8">
6663 <wpd-button>7</wpd-button>
6664 <wpd-button>8</wpd-button>
6665 <wpd-button>9</wpd-button>
6666 <wpd-button variant="primary">÷</wpd-button>
6667 <wpd-button>4</wpd-button>
6668 <wpd-button>5</wpd-button>
6669 <wpd-button>6</wpd-button>
6670 <wpd-button variant="primary">×</wpd-button>
6671 </wpd-grid>
6672 `
6673 };
6674 let WpdGrid = _WpdGrid;
6675 defineComponent("wpd-grid", WpdGrid);
6676 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}}`;
6677 const WPD_SPINNER_PRESETS = Object.freeze({
6678 classic: {
6679 sp1: 12,
6680 sp2: 24,
6681 sp3: 40,
6682 a1: 28,
6683 a2: 15,
6684 a3: 8,
6685 gap: 4,
6686 dir2: 1,
6687 dir3: -1,
6688 pulse: "none",
6689 dots: 0
6690 },
6691 comet: {
6692 sp1: 8,
6693 sp2: 14,
6694 sp3: 26,
6695 a1: 50,
6696 a2: 28,
6697 a3: 12,
6698 gap: 3,
6699 dir2: 1,
6700 dir3: 1,
6701 pulse: "none",
6702 dots: 5
6703 },
6704 orbit: {
6705 sp1: 10,
6706 sp2: 10,
6707 sp3: 32,
6708 a1: 50,
6709 a2: 50,
6710 a3: 8,
6711 gap: 5,
6712 dir2: -1,
6713 dir3: -1,
6714 pulse: "opacity",
6715 dots: 3
6716 },
6717 pulse: {
6718 sp1: 6,
6719 sp2: 18,
6720 sp3: 30,
6721 a1: 20,
6722 a2: 12,
6723 a3: 6,
6724 gap: 4,
6725 dir2: 1,
6726 dir3: -1,
6727 pulse: "both",
6728 dots: 8
6729 }
6730 });
6731 const CX = 61.26;
6732 const CY = 61.26;
6733 const DISC_R = 58.453;
6734 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"/>';
6735 const _WpdSpinner = class _WpdSpinner extends Component {
6736 constructor() {
6737 super(...arguments);
6738 this._paintScheduled = false;
6739 }
6740 connectedCallback() {
6741 super.connectedCallback();
6742 this._schedulePaint();
6743 }
6744 render() {
6745 return html`<div class="root" part="root"></div>`;
6746 }
6747 requestUpdate() {
6748 super.requestUpdate();
6749 this._schedulePaint();
6750 }
6751 _schedulePaint() {
6752 if (this._paintScheduled || !this.isConnected) {
6753 return;
6754 }
6755 this._paintScheduled = true;
6756 queueMicrotask(() => {
6757 this._paintScheduled = false;
6758 if (!this.isConnected) {
6759 return;
6760 }
6761 this._paint();
6762 });
6763 }
6764 _paint() {
6765 this._syncCssVars();
6766 const root = this.shadowRoot?.querySelector(
6767 ".root"
6768 );
6769 if (!root) {
6770 return;
6771 }
6772 root.innerHTML = this._buildSvg();
6773 }
6774 /**
6775 * Reflect the color / accent / size attributes onto CSS custom
6776 * properties on the host. Removing the attribute clears the var
6777 * so the default cascades back in.
6778 */
6779 _syncCssVars() {
6780 const sync = (attr, varName, transform) => {
6781 const v = this.getAttribute(attr);
6782 if (v === null) {
6783 this.style.removeProperty(varName);
6784 } else {
6785 this.style.setProperty(
6786 varName,
6787 transform ? transform(v) : v
6788 );
6789 }
6790 };
6791 sync("color", "--wpd-spinner-color");
6792 sync("accent", "--wpd-spinner-accent");
6793 sync(
6794 "size",
6795 "--wpd-spinner-size",
6796 (v) => /^-?\d+(\.\d+)?$/.test(v.trim()) ? `${v}px` : v
6797 );
6798 }
6799 _effectiveConfig() {
6800 const presetName = this.getAttribute("preset") ?? "classic";
6801 const preset = WPD_SPINNER_PRESETS[presetName] ?? WPD_SPINNER_PRESETS.classic;
6802 const num = (attr, fallback) => {
6803 const v = this.getAttribute(attr);
6804 if (v === null) {
6805 return fallback;
6806 }
6807 const n = parseFloat(v);
6808 return Number.isFinite(n) ? n : fallback;
6809 };
6810 const dir = (attr, fallback) => {
6811 const v = this.getAttribute(attr);
6812 if (v === null) {
6813 return fallback;
6814 }
6815 const lc = v.toLowerCase();
6816 if (lc === "-1" || lc === "ccw" || lc === "reverse") {
6817 return -1;
6818 }
6819 return 1;
6820 };
6821 const pulse = () => {
6822 const v = this.getAttribute("pulse");
6823 if (v === "scale" || v === "opacity" || v === "both" || v === "none") {
6824 return v;
6825 }
6826 return preset.pulse;
6827 };
6828 return {
6829 sp1: num("sp1", preset.sp1),
6830 sp2: num("sp2", preset.sp2),
6831 sp3: num("sp3", preset.sp3),
6832 a1: num("a1", preset.a1),
6833 a2: num("a2", preset.a2),
6834 a3: num("a3", preset.a3),
6835 gap: num("gap", preset.gap),
6836 dir2: dir("dir2", preset.dir2),
6837 dir3: dir("dir3", preset.dir3),
6838 pulse: pulse(),
6839 dots: Math.max(0, Math.floor(num("dots", preset.dots)))
6840 };
6841 }
6842 _buildSvg() {
6843 const cfg = this._effectiveConfig();
6844 const label = escAttr(this.getAttribute("label") ?? "Loading");
6845 const pad = cfg.gap * 3 + 14;
6846 const vbMin = -pad;
6847 const vbSize = 122.52 + pad * 2;
6848 const r1 = DISC_R + cfg.gap + 2;
6849 const r2 = r1 + cfg.gap + 2;
6850 const r3 = r2 + cfg.gap + 1.5;
6851 const ring1Anim = `animation: wpd-spinner-spin ${(cfg.sp1 / 10).toFixed(2)}s linear infinite`;
6852 const ring2Anim = `animation: wpd-spinner-spin ${(cfg.sp2 / 10).toFixed(2)}s linear infinite${cfg.dir2 < 0 ? " reverse" : ""}`;
6853 const ring3Anim = `animation: wpd-spinner-spin ${(cfg.sp3 / 10).toFixed(2)}s linear infinite${cfg.dir3 < 0 ? " reverse" : ""}`;
6854 const pspd = (cfg.sp1 * 1.8 / 10).toFixed(1);
6855 const ospd = (cfg.sp1 * 2.3 / 10).toFixed(1);
6856 let pulseStyle = "";
6857 if (cfg.pulse === "scale") {
6858 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite`;
6859 } else if (cfg.pulse === "opacity") {
6860 pulseStyle = `animation: wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
6861 } else if (cfg.pulse === "both") {
6862 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite, wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
6863 }
6864 let dotEls = "";
6865 if (cfg.dots > 0) {
6866 const dr = r3 + cfg.gap + 1;
6867 const dc2 = 2 * Math.PI * dr;
6868 const dsz = 1.6;
6869 const dotDur = (cfg.sp1 * 0.65 / 10).toFixed(2);
6870 for (let i = 0; i < cfg.dots; i++) {
6871 const offset = -(i / cfg.dots) * dc2;
6872 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"/>`;
6873 }
6874 }
6875 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>`;
6876 }
6877 };
6878 _WpdSpinner.props = [
6879 "preset",
6880 "size",
6881 "color",
6882 "accent",
6883 "sp1",
6884 "sp2",
6885 "sp3",
6886 "a1",
6887 "a2",
6888 "a3",
6889 "gap",
6890 "dir2",
6891 "dir3",
6892 "pulse",
6893 "dots",
6894 "label"
6895 ];
6896 _WpdSpinner.styles = [styles$3];
6897 _WpdSpinner.help = {
6898 title: "Spinner",
6899 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.",
6900 status: "experimental",
6901 since: "0.18.0",
6902 props: [
6903 {
6904 name: "preset",
6905 type: '"classic" | "comet" | "orbit" | "pulse"',
6906 default: "classic",
6907 description: "Visual personality. Every other attribute defaults to the preset's value and can be overridden individually."
6908 },
6909 {
6910 name: "size",
6911 type: "integer (px) or CSS length",
6912 default: "48",
6913 description: "Sets `--wpd-spinner-size`. Bare numbers are treated as px; pass a CSS length (e.g. `2em`) to opt into ems / rems."
6914 },
6915 {
6916 name: "color",
6917 type: "CSS color",
6918 description: "Disc + ring + dot color. Sets `--wpd-spinner-color`. Default inherits the WP admin theme color."
6919 },
6920 {
6921 name: "accent",
6922 type: "CSS color",
6923 default: "#fff",
6924 description: "Color of the W mark inside the disc. Sets `--wpd-spinner-accent`. Default white — change for dark-on-light or themed marks."
6925 },
6926 {
6927 name: "sp1, sp2, sp3",
6928 type: "integer (deciseconds)",
6929 description: "Per-ring rotation duration in tenths-of-a-second (12 → 1.2s). Higher = slower."
6930 },
6931 {
6932 name: "a1, a2, a3",
6933 type: "integer (0-100)",
6934 description: "Per-ring arc length as a percentage of the ring circumference."
6935 },
6936 {
6937 name: "gap",
6938 type: "integer",
6939 description: "Gap between concentric rings (units approximate to px at 120-viewport)."
6940 },
6941 {
6942 name: "dir2, dir3",
6943 type: '"1" | "-1" | "cw" | "ccw"',
6944 description: "Per-ring direction; ring 1 is always clockwise."
6945 },
6946 {
6947 name: "pulse",
6948 type: '"none" | "scale" | "opacity" | "both"',
6949 description: "Pulse animation applied to the disc + W mark."
6950 },
6951 {
6952 name: "dots",
6953 type: "integer",
6954 description: "Outer trailing dot count. Sensible values: 0, 3, 5, 8."
6955 },
6956 {
6957 name: "label",
6958 type: "string",
6959 default: "Loading",
6960 description: 'Accessible name for the SVG (`role="img"` + `aria-label`).'
6961 }
6962 ],
6963 cssProps: [
6964 { name: "--wpd-spinner-color", default: "var(--wp-admin-theme-color, #21759b)" },
6965 { name: "--wpd-spinner-accent", default: "#fff" },
6966 { name: "--wpd-spinner-size", default: "48px" }
6967 ],
6968 example: html`<wpd-spinner preset="comet" size="80"></wpd-spinner>`
6969 };
6970 let WpdSpinner = _WpdSpinner;
6971 function dasharray(r, pct) {
6972 const c = 2 * Math.PI * r;
6973 const visible = pct / 100 * c;
6974 const gap = c - visible;
6975 return `${visible.toFixed(2)} ${gap.toFixed(2)}`;
6976 }
6977 function escAttr(s) {
6978 return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6979 }
6980 defineComponent("wpd-spinner", WpdSpinner);
6981 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}`;
6982 let _cache = null;
6983 function parseCssContentToChar(raw) {
6984 let value = raw.trim();
6985 if (value === "") {
6986 return null;
6987 }
6988 if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
6989 value = value.slice(1, -1);
6990 }
6991 const escaped = value.match(/^\\([0-9a-f]{1,6})\s?$/i);
6992 if (escaped) {
6993 return String.fromCodePoint(parseInt(escaped[1], 16));
6994 }
6995 return value || null;
6996 }
6997 function buildMap() {
6998 const map = /* @__PURE__ */ new Map();
6999 if (typeof document === "undefined") {
7000 return map;
7001 }
7002 const sheets = Array.from(document.styleSheets ?? []);
7003 for (const sheet of sheets) {
7004 let rules = null;
7005 try {
7006 rules = sheet.cssRules;
7007 } catch {
7008 continue;
7009 }
7010 if (!rules) {
7011 continue;
7012 }
7013 for (const rule of Array.from(rules)) {
7014 const styleRule = rule;
7015 if (!styleRule || !styleRule.selectorText) {
7016 continue;
7017 }
7018 const match = styleRule.selectorText.match(
7019 /\.dashicons-([a-z0-9-]+)::?before/i
7020 );
7021 if (!match) {
7022 continue;
7023 }
7024 const content = styleRule.style?.content;
7025 if (!content) {
7026 continue;
7027 }
7028 const char = parseCssContentToChar(content);
7029 if (char) {
7030 map.set(match[1], char);
7031 }
7032 }
7033 }
7034 return map;
7035 }
7036 function resolveDashicon(name) {
7037 if (!_cache) {
7038 _cache = buildMap();
7039 }
7040 const slug = name.startsWith("dashicons-") ? name.slice("dashicons-".length) : name;
7041 return _cache.get(slug) ?? null;
7042 }
7043 function refreshDashiconCache() {
7044 _cache = buildMap();
7045 }
7046 let _scheduled = false;
7047 function primeOnLoad() {
7048 if (_scheduled || typeof window === "undefined") {
7049 return;
7050 }
7051 _scheduled = true;
7052 const refresh = () => {
7053 refreshDashiconCache();
7054 };
7055 if (document.readyState === "loading") {
7056 document.addEventListener("DOMContentLoaded", refresh, { once: true });
7057 }
7058 window.addEventListener("load", refresh, { once: true });
7059 }
7060 primeOnLoad();
7061 const _WpdIcon = class _WpdIcon extends Component {
7062 render() {
7063 const rawName = this.name || "";
7064 const slug = rawName.startsWith("dashicons-") ? rawName.slice("dashicons-".length) : rawName;
7065 const size = this.size;
7066 if (size && /^\d+$/.test(size)) {
7067 this.style.setProperty("--wpd-icon-size", `${size}px`);
7068 }
7069 const char = resolveDashicon(slug);
7070 if (char) {
7071 return html`<span
7072 class="wpd-icon__glyph wpd-icon__glyph--char dashicons dashicons-${slug}"
7073 aria-hidden="true"
7074 >${char}</span>`;
7075 }
7076 return html`<span
7077 class="wpd-icon__glyph dashicons dashicons-${slug}"
7078 aria-hidden="true"
7079 ></span>`;
7080 }
7081 };
7082 _WpdIcon.props = ["name", "size"];
7083 _WpdIcon.styles = [styles$2];
7084 _WpdIcon.help = {
7085 title: "Icon",
7086 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.',
7087 status: "stable",
7088 since: "0.10.0",
7089 props: [
7090 {
7091 name: "name",
7092 type: "string",
7093 description: "Dashicon identifier, with or without the `dashicons-` prefix."
7094 },
7095 {
7096 name: "size",
7097 type: "integer (px)",
7098 default: "16",
7099 description: "Glyph size in pixels."
7100 }
7101 ],
7102 cssProps: [
7103 { name: "--wpd-icon-size", default: "16px" }
7104 ],
7105 example: html`
7106 <wpd-cluster gap="8" align="center">
7107 <wpd-icon name="admin-post"></wpd-icon>
7108 <wpd-icon name="calculator" size="20"></wpd-icon>
7109 <wpd-icon name="dashicons-star-filled" size="32"></wpd-icon>
7110 </wpd-cluster>
7111 `
7112 };
7113 let WpdIcon = _WpdIcon;
7114 defineComponent("wpd-icon", WpdIcon);
7115 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}`;
7116 const _WpdEmptyState = class _WpdEmptyState extends Component {
7117 render() {
7118 const icon = this.icon || "";
7119 const heading = this.heading || "";
7120 const description = this.description || "";
7121 return html`
7122 ${icon ? html`<wpd-icon
7123 class="wpd-empty-state__icon"
7124 name=${icon}
7125 size="28"
7126 ></wpd-icon>` : null}
7127 <h3 class="wpd-empty-state__heading">${heading}</h3>
7128 <p class="wpd-empty-state__description">${description}</p>
7129 <div class="wpd-empty-state__cta">
7130 <slot name="cta"></slot>
7131 </div>
7132 <slot></slot>
7133 `;
7134 }
7135 };
7136 _WpdEmptyState.props = ["icon", "heading", "description"];
7137 _WpdEmptyState.styles = [styles$1];
7138 _WpdEmptyState.help = {
7139 title: "Empty state",
7140 summary: 'Centered placeholder for "nothing here yet" UI: icon + heading + description + optional CTA. A canonical shape so empty states look consistent across the shell.',
7141 status: "stable",
7142 since: "0.10.0",
7143 props: [
7144 {
7145 name: "icon",
7146 type: "string (dashicons slug)",
7147 description: "Dashicons identifier (with or without the dashicons- prefix)."
7148 },
7149 {
7150 name: "heading",
7151 type: "string",
7152 description: "Bold first line."
7153 },
7154 {
7155 name: "description",
7156 type: "string",
7157 description: "Secondary paragraph below the heading."
7158 }
7159 ],
7160 slots: [
7161 { name: "cta", description: "Call-to-action button row below the description." },
7162 { name: "(default)", description: "Any additional content rendered after the CTA." }
7163 ],
7164 cssProps: [
7165 { name: "--desktop-mode-text", description: "Heading colour." },
7166 { name: "--desktop-mode-muted", description: "Description colour." },
7167 { name: "--wpd-empty-state-fg" },
7168 { name: "--wpd-empty-state-icon-color" }
7169 ],
7170 example: html`
7171 <wpd-empty-state
7172 icon="admin-plugins"
7173 heading="No plugins installed yet"
7174 description="Install a plugin to see it here."
7175 >
7176 <wpd-button slot="cta" variant="primary">Browse plugins</wpd-button>
7177 </wpd-empty-state>
7178 `
7179 };
7180 let WpdEmptyState = _WpdEmptyState;
7181 defineComponent("wpd-empty-state", WpdEmptyState);
7182 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}}`;
7183 const _WpdRatingSummary = class _WpdRatingSummary extends Component {
7184 constructor() {
7185 super(...arguments);
7186 this._ratings = {};
7187 }
7188 /**
7189 * Per-star counts. Setting this triggers a re-render so consumers
7190 * can swap data without recreating the element.
7191 */
7192 get ratings() {
7193 return { ...this._ratings };
7194 }
7195 set ratings(next) {
7196 this._ratings = next ? { ...next } : {};
7197 this.requestUpdate();
7198 }
7199 render() {
7200 const rating = clamp01to100(numAttr(this, "rating"));
7201 const stars0to5 = rating / 100 * 5;
7202 const totalAttr = numAttr(this, "total");
7203 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);
7204 const fmt = new Intl.NumberFormat();
7205 const big = rating > 0 ? (rating / 100 * 5).toFixed(1) : "";
7206 return html`
7207 <div class="summary-card" role="img" aria-label=${ariaLabel(rating, total)}>
7208 <div class="summary">
7209 <div class="big">${big}</div>
7210 <div class="stars" aria-hidden="true">
7211 ${renderStarRow(stars0to5)}
7212 </div>
7213 <div class="total">
7214 ${total === 1 ? "1 rating" : `${fmt.format(total)} ratings`}
7215 </div>
7216 </div>
7217 <div class="bars">
7218 ${[5, 4, 3, 2, 1].map((star) => {
7219 const count = this._ratings[String(star)] ?? 0;
7220 const ratio = total === 0 ? 0 : count / total;
7221 return html`
7222 <div
7223 class="row"
7224 role="presentation"
7225 aria-label=${`${star} stars: ${fmt.format(count)}`}
7226 >
7227 <span class="row__label">
7228 ${star} ${filledStarSvg()}
7229 </span>
7230 <span class="row__track">
7231 <span
7232 class="row__fill"
7233 style=${`--ratio: ${ratio.toFixed(4)}`}
7234 ></span>
7235 </span>
7236 <span class="row__count">${fmt.format(count)}</span>
7237 </div>
7238 `;
7239 })}
7240 </div>
7241 </div>
7242 `;
7243 }
7244 };
7245 _WpdRatingSummary.props = ["rating", "total"];
7246 _WpdRatingSummary.styles = [styles];
7247 _WpdRatingSummary.help = {
7248 title: "Rating summary",
7249 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.",
7250 status: "experimental",
7251 since: "0.21.0",
7252 props: [
7253 {
7254 name: "rating",
7255 type: "number (0–100)",
7256 description: "Average rating on the wp.org 0–100 scale. Converted to a 0–5 display inside."
7257 },
7258 {
7259 name: "total",
7260 type: "number",
7261 description: "Total number of ratings. Optional — auto-summed from `ratings` when omitted."
7262 }
7263 ],
7264 cssProps: [
7265 { name: "--wpd-rating-fill", description: "Background of the bar fill." },
7266 { name: "--wpd-rating-track", description: "Background of the empty bar track." },
7267 { name: "--wpd-rating-star", description: "Color of filled stars." },
7268 { name: "--wpd-rating-star-empty", description: "Color of empty stars." },
7269 { name: "--wpd-rating-surface", description: "Card background." },
7270 { name: "--wpd-rating-border", description: "Card border color." },
7271 { name: "--wpd-rating-fg", description: "Primary text color." },
7272 { name: "--wpd-rating-fg-muted", description: "Secondary text color." }
7273 ],
7274 example: html`
7275 <wpd-rating-summary rating="92"></wpd-rating-summary>
7276 `
7277 };
7278 let WpdRatingSummary = _WpdRatingSummary;
7279 function numAttr(host, name) {
7280 const raw = host.getAttribute(name);
7281 if (raw === null || raw === "") {
7282 return 0;
7283 }
7284 const n = Number(raw);
7285 return Number.isFinite(n) ? n : 0;
7286 }
7287 function clamp01to100(n) {
7288 if (n < 0) {
7289 return 0;
7290 }
7291 if (n > 100) {
7292 return 100;
7293 }
7294 return n;
7295 }
7296 function ariaLabel(rating, total) {
7297 if (total === 0) {
7298 return "No ratings yet";
7299 }
7300 const stars = rating / 100 * 5;
7301 return `Average rating ${stars.toFixed(1)} out of 5, from ${total} ratings`;
7302 }
7303 function renderStarRow(stars0to5) {
7304 const full = Math.floor(stars0to5);
7305 const half = stars0to5 - full >= 0.5 ? 1 : 0;
7306 const empty = 5 - full - half;
7307 const list = [];
7308 for (let i = 0; i < full; i++) {
7309 list.push(filledStarSvg());
7310 }
7311 for (let i = 0; i < half; i++) {
7312 list.push(halfStarSvg());
7313 }
7314 for (let i = 0; i < empty; i++) {
7315 list.push(emptyStarSvg());
7316 }
7317 return list;
7318 }
7319 function filledStarSvg() {
7320 return html`
7321 <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
7322 <path
7323 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"
7324 />
7325 </svg>
7326 `;
7327 }
7328 function halfStarSvg() {
7329 return html`
7330 <svg viewBox="0 0 16 16" aria-hidden="true">
7331 <defs>
7332 <linearGradient id="wpd-half-star">
7333 <stop offset="50%" stop-color="currentColor" />
7334 <stop
7335 offset="50%"
7336 stop-color="currentColor"
7337 stop-opacity="0.22"
7338 />
7339 </linearGradient>
7340 </defs>
7341 <path
7342 fill="url(#wpd-half-star)"
7343 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"
7344 />
7345 </svg>
7346 `;
7347 }
7348 function emptyStarSvg() {
7349 return html`
7350 <svg
7351 class="empty"
7352 viewBox="0 0 16 16"
7353 fill="currentColor"
7354 aria-hidden="true"
7355 >
7356 <path
7357 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"
7358 />
7359 </svg>
7360 `;
7361 }
7362 defineComponent("wpd-rating-summary", WpdRatingSummary);
7363 const wpOrgCache = /* @__PURE__ */ new Map();
7364 const reviewsCache = /* @__PURE__ */ new Map();
7365 function buildInstalledDetail(row) {
7366 const root = document.createElement("div");
7367 root.className = "desktop-mode-plugins__detail";
7368 root.setAttribute("data-noclick", "");
7369 const style = document.createElement("style");
7370 style.textContent = PANEL_STYLES;
7371 root.appendChild(style);
7372 const slug = deriveSlug(row);
7373 root.appendChild(buildHero(row));
7374 const tabsHost = document.createElement("div");
7375 tabsHost.className = "desktop-mode-plugins__detail-tabs-wrap";
7376 root.appendChild(tabsHost);
7377 const body = document.createElement("div");
7378 body.className = "desktop-mode-plugins__detail-body";
7379 root.appendChild(body);
7380 const tabs = document.createElement("wpd-tabs");
7381 tabs.className = "desktop-mode-plugins__detail-tabs";
7382 tabs.setAttribute("value", "overview");
7383 const tabDefs = [
7384 { value: "overview", label: __("Overview", "desktop-mode"), show: true },
7385 { value: "details", label: __("Details", "desktop-mode"), show: true },
7386 { value: "changelog", label: __("Changelog", "desktop-mode"), show: !!slug },
7387 { value: "faq", label: __("FAQ", "desktop-mode"), show: !!slug },
7388 { value: "reviews", label: __("Reviews", "desktop-mode"), show: !!slug }
7389 ];
7390 for (const def of tabDefs) {
7391 if (!def.show) {
7392 continue;
7393 }
7394 const tab = document.createElement("wpd-tab");
7395 tab.setAttribute("value", def.value);
7396 tab.textContent = def.label;
7397 tabs.appendChild(tab);
7398 }
7399 tabsHost.appendChild(tabs);
7400 let info = slug ? wpOrgCache.get(slug) ?? null : null;
7401 let infoFetching = false;
7402 let active = "overview";
7403 const ensureInfo = () => {
7404 if (!slug || info || infoFetching) {
7405 return;
7406 }
7407 infoFetching = true;
7408 void (async () => {
7409 try {
7410 info = await fetchPluginInfo(slug);
7411 wpOrgCache.set(slug, info);
7412 if (root.isConnected) {
7413 paintActive();
7414 }
7415 } catch {
7416 if (root.isConnected) {
7417 paintActive();
7418 }
7419 } finally {
7420 infoFetching = false;
7421 }
7422 })();
7423 };
7424 const paintActive = () => {
7425 body.replaceChildren(renderTab(active, row, slug, info));
7426 };
7427 tabs.addEventListener("wpd-tab-change", (ev) => {
7428 const detail = ev.detail;
7429 active = detail?.value ?? "overview";
7430 if (slug && active !== "overview" && active !== "details") {
7431 ensureInfo();
7432 }
7433 paintActive();
7434 });
7435 paintActive();
7436 return root;
7437 }
7438 function buildHero(row, _slug) {
7439 const hero = document.createElement("div");
7440 hero.className = "desktop-mode-plugins__detail-hero";
7441 const inner = document.createElement("div");
7442 inner.className = "desktop-mode-plugins__detail-hero-inner";
7443 const iconTile = document.createElement("div");
7444 iconTile.className = "desktop-mode-plugins__detail-hero-icon";
7445 const iconUrl = row.desktop_mode_icon_url;
7446 if (iconUrl) {
7447 const img = document.createElement("img");
7448 img.alt = "";
7449 img.loading = "lazy";
7450 img.decoding = "async";
7451 img.src = attachIconFallback(img, iconUrl, () => {
7452 iconTile.replaceChildren(buildFallbackGlyph());
7453 });
7454 iconTile.appendChild(img);
7455 } else {
7456 iconTile.appendChild(buildFallbackGlyph());
7457 }
7458 const titleBlock = document.createElement("wpd-stack");
7459 titleBlock.setAttribute("gap", "6");
7460 titleBlock.className = "desktop-mode-plugins__detail-hero-text";
7461 const titleRow = document.createElement("wpd-cluster");
7462 titleRow.setAttribute("gap", "10");
7463 titleRow.setAttribute("align", "center");
7464 const title = document.createElement("h3");
7465 title.className = "desktop-mode-plugins__detail-title";
7466 title.textContent = row.name || row.plugin;
7467 titleRow.appendChild(title);
7468 if (row.version) {
7469 const ver = document.createElement("wpd-badge");
7470 ver.setAttribute("tone", "neutral");
7471 ver.setAttribute("no-dot", "");
7472 ver.textContent = sprintf(
7473 /* translators: %s: version number */
7474 __("v%s", "desktop-mode"),
7475 row.version
7476 );
7477 titleRow.appendChild(ver);
7478 }
7479 const isActive = row.status === "active" || row.status === "active-network";
7480 const statusBadge = document.createElement("wpd-badge");
7481 statusBadge.setAttribute("tone", isActive ? "success" : "neutral");
7482 statusBadge.textContent = isActive ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode");
7483 titleRow.appendChild(statusBadge);
7484 const update = row.desktop_mode_update_available;
7485 if (update?.available && update.new_version) {
7486 const upd = document.createElement("wpd-badge");
7487 upd.setAttribute("tone", "warning");
7488 upd.textContent = sprintf(
7489 /* translators: %s: new version available */
7490 __("Update to %s", "desktop-mode"),
7491 update.new_version
7492 );
7493 titleRow.appendChild(upd);
7494 }
7495 titleBlock.appendChild(titleRow);
7496 const byline = document.createElement("p");
7497 byline.className = "desktop-mode-plugins__detail-byline";
7498 const authorText = stripHtml$1(row.author ?? "") || __("Unknown author", "desktop-mode");
7499 if (row.author_uri) {
7500 const a = document.createElement("a");
7501 a.href = row.author_uri;
7502 a.target = "_blank";
7503 a.rel = "noopener noreferrer";
7504 a.textContent = authorText;
7505 a.setAttribute("data-noclick", "");
7506 byline.append(__("by", "desktop-mode") + " ", a);
7507 } else {
7508 byline.textContent = sprintf(
7509 /* translators: %s: plugin author */
7510 __("by %s", "desktop-mode"),
7511 authorText
7512 );
7513 }
7514 titleBlock.appendChild(byline);
7515 inner.append(iconTile, titleBlock);
7516 hero.appendChild(inner);
7517 return hero;
7518 }
7519 function renderTab(tab, row, slug, info) {
7520 if (tab === "overview") {
7521 return renderOverview(row, slug, info);
7522 }
7523 if (tab === "details") {
7524 return renderDetails(row);
7525 }
7526 if (tab === "changelog") {
7527 return renderChangelog(info);
7528 }
7529 if (tab === "faq") {
7530 return renderFaq(info);
7531 }
7532 return renderReviews(slug, info);
7533 }
7534 function renderOverview(row, slug, info) {
7535 const stack = document.createElement("wpd-stack");
7536 stack.setAttribute("gap", "20");
7537 const chipStrip = buildOverviewChips(row, info);
7538 if (chipStrip.children.length > 0) {
7539 stack.appendChild(chipStrip);
7540 }
7541 const descHtml = info?.sections?.description ?? info?.short_description ?? readDescription(row);
7542 if (descHtml) {
7543 const desc = document.createElement("div");
7544 desc.className = "desktop-mode-plugins__detail-html";
7545 desc.innerHTML = sanitizeHtml(descHtml);
7546 sanitizeLinks(desc);
7547 stack.appendChild(desc);
7548 } else if (slug && !info) {
7549 stack.appendChild(buildLoadingBlock(__("Loading description…", "desktop-mode")));
7550 } else {
7551 stack.appendChild(
7552 buildEmpty(
7553 "admin-plugins",
7554 __("No description", "desktop-mode"),
7555 __("This plugin doesn’t ship a description in its header.", "desktop-mode")
7556 )
7557 );
7558 }
7559 const actions = document.createElement("wpd-cluster");
7560 actions.setAttribute("gap", "8");
7561 actions.className = "desktop-mode-plugins__detail-actions";
7562 if (slug) {
7563 actions.appendChild(
7564 linkButton(
7565 "primary",
7566 __("View on WordPress.org", "desktop-mode"),
7567 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/`
7568 )
7569 );
7570 }
7571 if (row.plugin_uri) {
7572 actions.appendChild(
7573 linkButton("secondary", __("Plugin website", "desktop-mode"), row.plugin_uri)
7574 );
7575 }
7576 if (row.author_uri) {
7577 actions.appendChild(
7578 linkButton("ghost", __("Author website", "desktop-mode"), row.author_uri)
7579 );
7580 }
7581 if (actions.children.length > 0) {
7582 stack.appendChild(actions);
7583 }
7584 return stack;
7585 }
7586 function buildOverviewChips(row, info) {
7587 const strip = document.createElement("wpd-cluster");
7588 strip.setAttribute("gap", "8");
7589 strip.className = "desktop-mode-plugins__detail-chip-strip";
7590 if (info) {
7591 if (typeof info.rating === "number" && info.rating > 0) {
7592 const stars = document.createElement("span");
7593 stars.className = "desktop-mode-plugins__detail-stars-pill";
7594 stars.appendChild(buildStarCluster(info.rating, info.num_ratings ?? 0));
7595 strip.appendChild(stars);
7596 }
7597 if (info.active_installs) {
7598 strip.appendChild(
7599 chip(
7600 "admin-users",
7601 sprintf(
7602 /* translators: %s: comma-grouped active install count */
7603 __("%s+ active installs", "desktop-mode"),
7604 new Intl.NumberFormat().format(info.active_installs)
7605 )
7606 )
7607 );
7608 }
7609 if (info.last_updated) {
7610 strip.appendChild(
7611 chip(
7612 "update",
7613 sprintf(
7614 /* translators: %s: date the plugin was last updated */
7615 __("Updated %s", "desktop-mode"),
7616 humanDate(info.last_updated)
7617 )
7618 )
7619 );
7620 }
7621 if (info.tested) {
7622 strip.appendChild(
7623 chip(
7624 "wordpress-alt",
7625 sprintf(
7626 /* translators: %s: maximum tested WordPress version */
7627 __("Tested up to WP %s", "desktop-mode"),
7628 info.tested
7629 )
7630 )
7631 );
7632 }
7633 }
7634 if (row.requires_wp) {
7635 strip.appendChild(
7636 chip(
7637 "wordpress",
7638 sprintf(
7639 /* translators: %s: minimum WordPress version */
7640 __("Requires WP %s+", "desktop-mode"),
7641 row.requires_wp
7642 )
7643 )
7644 );
7645 }
7646 if (row.requires_php) {
7647 strip.appendChild(
7648 chip(
7649 "editor-code",
7650 sprintf(
7651 /* translators: %s: minimum PHP version */
7652 __("Requires PHP %s+", "desktop-mode"),
7653 row.requires_php
7654 )
7655 )
7656 );
7657 }
7658 if (row.network_only) {
7659 strip.appendChild(
7660 chip("networking", __("Network only", "desktop-mode"))
7661 );
7662 }
7663 return strip;
7664 }
7665 function renderDetails(row) {
7666 const grid = document.createElement("wpd-grid");
7667 grid.setAttribute("columns", "2");
7668 grid.setAttribute("gap", "12");
7669 grid.className = "desktop-mode-plugins__detail-grid";
7670 pushFactCard(grid, "media-document", __("Plugin file", "desktop-mode"), codeNode(row.plugin));
7671 if (row.version) {
7672 pushFactCard(grid, "tag", __("Version", "desktop-mode"), row.version);
7673 }
7674 if (row.desktop_mode_size_kb !== null && row.desktop_mode_size_kb !== void 0) {
7675 pushFactCard(
7676 grid,
7677 "database",
7678 __("Size on disk", "desktop-mode"),
7679 formatSize$1(row.desktop_mode_size_kb)
7680 );
7681 }
7682 if (row.requires_wp) {
7683 pushFactCard(
7684 grid,
7685 "wordpress-alt",
7686 __("Requires WordPress", "desktop-mode"),
7687 sprintf(
7688 /* translators: %s: version */
7689 __("%s+", "desktop-mode"),
7690 row.requires_wp
7691 )
7692 );
7693 }
7694 if (row.requires_php) {
7695 pushFactCard(
7696 grid,
7697 "editor-code",
7698 __("Requires PHP", "desktop-mode"),
7699 sprintf(
7700 /* translators: %s: version */
7701 __("%s+", "desktop-mode"),
7702 row.requires_php
7703 )
7704 );
7705 }
7706 if (row.textdomain) {
7707 pushFactCard(
7708 grid,
7709 "translation",
7710 __("Text domain", "desktop-mode"),
7711 codeNode(String(row.textdomain))
7712 );
7713 }
7714 if (row.plugin_uri) {
7715 pushFactCard(grid, "admin-links", __("Plugin URL", "desktop-mode"), externalLink(row.plugin_uri));
7716 }
7717 if (row.author_uri) {
7718 pushFactCard(grid, "admin-users", __("Author URL", "desktop-mode"), externalLink(row.author_uri));
7719 }
7720 if (row.network_only) {
7721 pushFactCard(
7722 grid,
7723 "networking",
7724 __("Scope", "desktop-mode"),
7725 __("Network only", "desktop-mode")
7726 );
7727 }
7728 pushFactCard(
7729 grid,
7730 row.status === "active" || row.status === "active-network" ? "yes-alt" : "marker",
7731 __("Status", "desktop-mode"),
7732 row.status === "active" || row.status === "active-network" ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode")
7733 );
7734 return grid;
7735 }
7736 function pushFactCard(parent, icon, label, value) {
7737 const card = document.createElement("wpd-card");
7738 card.setAttribute("compact", "");
7739 card.className = "desktop-mode-plugins__detail-fact";
7740 const head = document.createElement("div");
7741 head.setAttribute("slot", "header");
7742 head.className = "desktop-mode-plugins__detail-fact-head";
7743 const ico = document.createElement("span");
7744 ico.className = `dashicons dashicons-${icon}`;
7745 ico.setAttribute("aria-hidden", "true");
7746 const lab = document.createElement("span");
7747 lab.className = "desktop-mode-plugins__detail-fact-label";
7748 lab.textContent = label;
7749 head.append(ico, lab);
7750 card.appendChild(head);
7751 const val = document.createElement("div");
7752 val.className = "desktop-mode-plugins__detail-fact-value";
7753 if (typeof value === "string") {
7754 val.textContent = value;
7755 } else {
7756 val.appendChild(value);
7757 }
7758 card.appendChild(val);
7759 parent.appendChild(card);
7760 }
7761 function renderChangelog(info) {
7762 if (!info) {
7763 return buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode"));
7764 }
7765 const html2 = info.sections?.changelog;
7766 if (!html2) {
7767 return buildEmpty(
7768 "list-view",
7769 __("No changelog", "desktop-mode"),
7770 __("This plugin doesn’t ship a changelog.", "desktop-mode")
7771 );
7772 }
7773 const entries = parseChangelogEntries(html2);
7774 if (entries.length === 0) {
7775 const wrap = document.createElement("div");
7776 wrap.className = "desktop-mode-plugins__detail-html";
7777 wrap.innerHTML = sanitizeHtml(html2);
7778 sanitizeLinks(wrap);
7779 return wrap;
7780 }
7781 const stack = document.createElement("wpd-stack");
7782 stack.setAttribute("gap", "12");
7783 stack.className = "desktop-mode-plugins__detail-changelog";
7784 entries.forEach((entry, i) => {
7785 const card = document.createElement("wpd-card");
7786 card.className = "desktop-mode-plugins__detail-changelog-entry";
7787 const head = document.createElement("div");
7788 head.setAttribute("slot", "header");
7789 head.className = "desktop-mode-plugins__detail-changelog-head";
7790 const ver = document.createElement("wpd-badge");
7791 ver.setAttribute("tone", i === 0 ? "success" : "neutral");
7792 ver.textContent = entry.version;
7793 head.appendChild(ver);
7794 if (i === 0) {
7795 const latest = document.createElement("span");
7796 latest.className = "desktop-mode-plugins__detail-changelog-latest";
7797 latest.textContent = __("Latest", "desktop-mode");
7798 head.appendChild(latest);
7799 }
7800 card.appendChild(head);
7801 const body = document.createElement("div");
7802 body.className = "desktop-mode-plugins__detail-html";
7803 body.innerHTML = sanitizeHtml(entry.body);
7804 sanitizeLinks(body);
7805 card.appendChild(body);
7806 stack.appendChild(card);
7807 });
7808 return stack;
7809 }
7810 function parseChangelogEntries(html2) {
7811 const tmp = document.createElement("div");
7812 tmp.innerHTML = html2;
7813 if (tmp.childNodes.length === 0) {
7814 return [];
7815 }
7816 const entries = [];
7817 let current = null;
7818 const versionRegex = /([0-9]+\.[0-9]+(?:\.[0-9]+)?(?:[\w.+-]*)?)/;
7819 const flush = () => {
7820 if (!current) {
7821 return;
7822 }
7823 entries.push({ version: current.version, body: current.html.trim() });
7824 current = null;
7825 };
7826 for (const node of Array.from(tmp.childNodes)) {
7827 if (node.nodeType === Node.ELEMENT_NODE) {
7828 const el = node;
7829 const isHeading = /^H[1-6]$/.test(el.tagName);
7830 const text = (el.textContent ?? "").trim();
7831 const cleaned = text.replace(/^=+\s*|\s*=+$/g, "").trim();
7832 const headingMatch = isHeading ? cleaned.match(versionRegex) : null;
7833 if (headingMatch) {
7834 flush();
7835 current = { version: cleaned, html: "" };
7836 continue;
7837 }
7838 if (!current) {
7839 continue;
7840 }
7841 current.html += el.outerHTML;
7842 continue;
7843 }
7844 if (node.nodeType === Node.TEXT_NODE) {
7845 const text = node.textContent ?? "";
7846 if (!current) {
7847 continue;
7848 }
7849 if (text.trim() === "") {
7850 if (current.html !== "") {
7851 current.html += text;
7852 }
7853 continue;
7854 }
7855 current.html += `<p>${escapeHtml$1(text)}</p>`;
7856 }
7857 }
7858 flush();
7859 return entries;
7860 }
7861 function renderFaq(info) {
7862 if (!info) {
7863 return buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode"));
7864 }
7865 const html2 = info.sections?.faq;
7866 if (!html2) {
7867 return buildEmpty(
7868 "editor-help",
7869 __("No FAQ", "desktop-mode"),
7870 __("This plugin doesn’t ship an FAQ.", "desktop-mode")
7871 );
7872 }
7873 const pairs = parseFaqPairs(html2);
7874 if (pairs.length === 0) {
7875 const wrap = document.createElement("div");
7876 wrap.className = "desktop-mode-plugins__detail-html";
7877 wrap.innerHTML = sanitizeHtml(html2);
7878 sanitizeLinks(wrap);
7879 return wrap;
7880 }
7881 const stack = document.createElement("wpd-stack");
7882 stack.setAttribute("gap", "8");
7883 stack.className = "desktop-mode-plugins__detail-faq";
7884 pairs.forEach((pair, i) => {
7885 const item = document.createElement("details");
7886 item.className = "desktop-mode-plugins__detail-faq-item";
7887 if (i === 0) {
7888 item.setAttribute("open", "");
7889 }
7890 const summary = document.createElement("summary");
7891 summary.className = "desktop-mode-plugins__detail-faq-q";
7892 const qText = document.createElement("span");
7893 qText.className = "desktop-mode-plugins__detail-faq-q-text";
7894 qText.textContent = pair.question;
7895 const chevron = document.createElement("span");
7896 chevron.className = "desktop-mode-plugins__detail-faq-chevron";
7897 chevron.setAttribute("aria-hidden", "true");
7898 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>';
7899 summary.append(qText, chevron);
7900 const body = document.createElement("div");
7901 body.className = "desktop-mode-plugins__detail-faq-a desktop-mode-plugins__detail-html";
7902 body.innerHTML = sanitizeHtml(pair.answer);
7903 sanitizeLinks(body);
7904 item.append(summary, body);
7905 stack.appendChild(item);
7906 });
7907 return stack;
7908 }
7909 function parseFaqPairs(html2) {
7910 const tmp = document.createElement("div");
7911 tmp.innerHTML = html2;
7912 const dts = Array.from(tmp.querySelectorAll(":scope > dt"));
7913 if (dts.length > 0) {
7914 const pairs2 = [];
7915 for (const dt of dts) {
7916 pairs2.push(splitDtIntoPair(dt));
7917 }
7918 return pairs2.filter((p) => p.question !== "");
7919 }
7920 const dl = tmp.querySelector(":scope > dl");
7921 if (dl) {
7922 const pairs2 = [];
7923 let current2 = null;
7924 for (const node of Array.from(dl.children)) {
7925 if (node.tagName === "DT") {
7926 if (current2) {
7927 pairs2.push({ question: current2.q, answer: current2.html.trim() });
7928 }
7929 current2 = { q: (node.textContent ?? "").trim(), html: "" };
7930 } else if (node.tagName === "DD" && current2) {
7931 current2.html += node.innerHTML;
7932 } else if (current2) {
7933 current2.html += node.outerHTML;
7934 }
7935 }
7936 if (current2) {
7937 pairs2.push({ question: current2.q, answer: current2.html.trim() });
7938 }
7939 return pairs2.filter((p) => p.question !== "");
7940 }
7941 const pairs = [];
7942 let current = null;
7943 const flush = () => {
7944 if (!current) {
7945 return;
7946 }
7947 pairs.push({ question: current.q, answer: current.html.trim() });
7948 current = null;
7949 };
7950 for (const node of Array.from(tmp.childNodes)) {
7951 if (node.nodeType === Node.ELEMENT_NODE) {
7952 const el = node;
7953 const isHeading = /^H[1-6]$/.test(el.tagName);
7954 const text = (el.textContent ?? "").trim();
7955 if (isHeading && text) {
7956 flush();
7957 current = { q: text, html: "" };
7958 continue;
7959 }
7960 if (!current) {
7961 continue;
7962 }
7963 current.html += el.outerHTML;
7964 continue;
7965 }
7966 if (node.nodeType === Node.TEXT_NODE && current) {
7967 const text = node.textContent ?? "";
7968 if (text.trim() === "") {
7969 if (current.html !== "") {
7970 current.html += text;
7971 }
7972 continue;
7973 }
7974 current.html += `<p>${escapeHtml$1(text)}</p>`;
7975 }
7976 }
7977 flush();
7978 return pairs.filter((p) => p.question !== "");
7979 }
7980 function splitDtIntoPair(dt) {
7981 let question = "";
7982 let answerHtml = "";
7983 let seenElement = false;
7984 for (const child of Array.from(dt.childNodes)) {
7985 if (child.nodeType === Node.TEXT_NODE) {
7986 if (!seenElement) {
7987 question += child.textContent ?? "";
7988 } else {
7989 const txt = child.textContent ?? "";
7990 if (txt.trim() !== "") {
7991 answerHtml += `<p>${escapeHtml$1(txt)}</p>`;
7992 }
7993 }
7994 continue;
7995 }
7996 if (child.nodeType !== Node.ELEMENT_NODE) {
7997 continue;
7998 }
7999 const el = child;
8000 if (el.tagName === "P" && (el.textContent ?? "").trim() === "") {
8001 continue;
8002 }
8003 seenElement = true;
8004 answerHtml += el.outerHTML;
8005 }
8006 return {
8007 question: question.replace(/\s+/g, " ").trim(),
8008 answer: answerHtml.trim()
8009 };
8010 }
8011 function escapeHtml$1(text) {
8012 const tmp = document.createElement("span");
8013 tmp.textContent = text;
8014 return tmp.innerHTML;
8015 }
8016 function renderReviews(slug, info) {
8017 const stack = document.createElement("wpd-stack");
8018 stack.setAttribute("gap", "16");
8019 if (!info) {
8020 stack.appendChild(buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode")));
8021 return stack;
8022 }
8023 stack.appendChild(buildHistogram(info));
8024 const body = document.createElement("div");
8025 body.className = "desktop-mode-plugins__detail-reviews";
8026 stack.appendChild(body);
8027 const cached = reviewsCache.get(slug);
8028 if (cached) {
8029 paintReviewList(body, cached, slug);
8030 } else {
8031 body.appendChild(buildLoadingBlock(__("Loading recent reviews…", "desktop-mode")));
8032 void (async () => {
8033 try {
8034 const resp = await fetchPluginReviews(slug);
8035 reviewsCache.set(slug, resp);
8036 if (body.isConnected) {
8037 paintReviewList(body, resp, slug);
8038 }
8039 } catch {
8040 if (body.isConnected) {
8041 body.replaceChildren(
8042 buildEmpty(
8043 "warning",
8044 __("Couldn’t load reviews", "desktop-mode"),
8045 __("WordPress.org didn’t respond. Try again in a moment.", "desktop-mode")
8046 )
8047 );
8048 }
8049 }
8050 })();
8051 }
8052 return stack;
8053 }
8054 function paintReviewList(host, resp, slug) {
8055 host.replaceChildren();
8056 if (!resp.parsed) {
8057 host.appendChild(buildReviewsFallback(slug));
8058 return;
8059 }
8060 if (resp.items.length === 0) {
8061 host.appendChild(buildWriteReviewCta(slug));
8062 return;
8063 }
8064 const grid = document.createElement("wpd-grid");
8065 grid.setAttribute("columns", "2");
8066 grid.setAttribute("gap", "12");
8067 grid.className = "desktop-mode-plugins__detail-reviews-grid";
8068 for (const item of resp.items) {
8069 grid.appendChild(buildReviewCard(item));
8070 }
8071 host.appendChild(grid);
8072 const more = document.createElement("div");
8073 more.className = "desktop-mode-plugins__detail-reviews-more";
8074 more.appendChild(
8075 linkButton(
8076 "ghost",
8077 __("Read all reviews on WordPress.org ↗", "desktop-mode"),
8078 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/#reviews`
8079 )
8080 );
8081 host.appendChild(more);
8082 }
8083 function buildReviewsFallback(slug) {
8084 const empty = buildEmpty(
8085 "external",
8086 __("Reviews live on WordPress.org", "desktop-mode"),
8087 __(
8088 "We couldn’t pull the review feed here. Open the full thread on WordPress.org to read every review.",
8089 "desktop-mode"
8090 )
8091 );
8092 const cta = document.createElement("wpd-button");
8093 cta.setAttribute("slot", "cta");
8094 cta.setAttribute("variant", "primary");
8095 cta.setAttribute("size", "small");
8096 cta.setAttribute("data-noclick", "");
8097 cta.textContent = __("Open reviews on WordPress.org ↗", "desktop-mode");
8098 cta.addEventListener("click", () => {
8099 window.open(
8100 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/#reviews`,
8101 "_blank",
8102 "noopener,noreferrer"
8103 );
8104 });
8105 empty.appendChild(cta);
8106 return empty;
8107 }
8108 function buildWriteReviewCta(slug) {
8109 const wrap = document.createElement("div");
8110 wrap.className = "desktop-mode-plugins__detail-reviews-cta";
8111 wrap.appendChild(
8112 linkButton(
8113 "primary",
8114 __("Write a review on WordPress.org ↗", "desktop-mode"),
8115 `https://wordpress.org/support/plugin/${encodeURIComponent(slug)}/reviews/#new-post`
8116 )
8117 );
8118 return wrap;
8119 }
8120 function buildReviewCard(item) {
8121 const card = document.createElement("wpd-card");
8122 card.setAttribute("compact", "");
8123 card.className = "desktop-mode-plugins__detail-review";
8124 const head = document.createElement("div");
8125 head.setAttribute("slot", "header");
8126 head.className = "desktop-mode-plugins__detail-review-head";
8127 const author = document.createElement("strong");
8128 author.textContent = item.author || __("Anonymous", "desktop-mode");
8129 head.appendChild(author);
8130 const stars = buildStarCluster(item.stars / 5 * 100, 0);
8131 head.appendChild(stars);
8132 if (item.date) {
8133 const date = document.createElement("span");
8134 date.className = "desktop-mode-plugins__detail-review-date";
8135 date.textContent = item.date;
8136 head.appendChild(date);
8137 }
8138 card.appendChild(head);
8139 if (item.excerpt) {
8140 const body = document.createElement("p");
8141 body.className = "desktop-mode-plugins__detail-review-body";
8142 body.textContent = item.excerpt;
8143 card.appendChild(body);
8144 }
8145 if (item.url) {
8146 const foot = document.createElement("div");
8147 foot.setAttribute("slot", "footer");
8148 const link = document.createElement("a");
8149 link.href = item.url;
8150 link.target = "_blank";
8151 link.rel = "noopener noreferrer";
8152 link.setAttribute("data-noclick", "");
8153 link.textContent = __("Read full review ↗", "desktop-mode");
8154 link.className = "desktop-mode-plugins__detail-review-link";
8155 foot.appendChild(link);
8156 card.appendChild(foot);
8157 }
8158 return card;
8159 }
8160 function buildHistogram(info) {
8161 const el = document.createElement("wpd-rating-summary");
8162 if (typeof info.rating === "number") {
8163 el.setAttribute("rating", String(info.rating));
8164 }
8165 if (info.num_ratings) {
8166 el.setAttribute("total", String(info.num_ratings));
8167 }
8168 const buckets = {};
8169 const ratings = info.ratings ?? {};
8170 for (const key of ["1", "2", "3", "4", "5"]) {
8171 const v = ratings[key];
8172 if (typeof v === "number") {
8173 buckets[key] = v;
8174 }
8175 }
8176 el.ratings = buckets;
8177 return el;
8178 }
8179 function chip(icon, label) {
8180 const c = document.createElement("wpd-chip");
8181 c.setAttribute("label", label);
8182 c.setAttribute("tone", "neutral");
8183 const ico = document.createElement("span");
8184 ico.setAttribute("slot", "icon");
8185 ico.className = `dashicons dashicons-${icon}`;
8186 ico.setAttribute("aria-hidden", "true");
8187 c.appendChild(ico);
8188 return c;
8189 }
8190 function buildLoadingBlock(label) {
8191 const wrap = document.createElement("div");
8192 wrap.className = "desktop-mode-plugins__detail-loading-block";
8193 const spinner = document.createElement("wpd-spinner");
8194 spinner.setAttribute("preset", "classic");
8195 spinner.setAttribute("size", "20");
8196 wrap.appendChild(spinner);
8197 const text = document.createElement("span");
8198 text.textContent = label;
8199 wrap.appendChild(text);
8200 return wrap;
8201 }
8202 function buildEmpty(icon, heading, description) {
8203 const e = document.createElement("wpd-empty-state");
8204 e.setAttribute("icon", `dashicons-${icon}`);
8205 e.setAttribute("heading", heading);
8206 e.setAttribute("description", description);
8207 return e;
8208 }
8209 function buildFallbackGlyph() {
8210 const span = document.createElement("span");
8211 span.className = "dashicons dashicons-admin-plugins";
8212 span.setAttribute("aria-hidden", "true");
8213 return span;
8214 }
8215 function linkButton(variant, label, href) {
8216 const btn = document.createElement("wpd-button");
8217 btn.setAttribute("variant", variant);
8218 btn.setAttribute("size", "small");
8219 btn.textContent = label;
8220 btn.setAttribute("data-noclick", "");
8221 btn.addEventListener("click", () => {
8222 window.open(href, "_blank", "noopener,noreferrer");
8223 });
8224 return btn;
8225 }
8226 function codeNode(text) {
8227 const code = document.createElement("code");
8228 code.textContent = text;
8229 return code;
8230 }
8231 function externalLink(href) {
8232 const a = document.createElement("a");
8233 a.href = href;
8234 a.target = "_blank";
8235 a.rel = "noopener noreferrer";
8236 a.textContent = href;
8237 a.setAttribute("data-noclick", "");
8238 return a;
8239 }
8240 function sanitizeLinks(wrap) {
8241 wrap.querySelectorAll("a").forEach((a) => {
8242 a.setAttribute("target", "_blank");
8243 a.setAttribute("rel", "noopener noreferrer");
8244 a.setAttribute("data-noclick", "");
8245 });
8246 }
8247 function deriveSlug(row) {
8248 if (!row.desktop_mode_icon_url) {
8249 return "";
8250 }
8251 const fromUpdate = row.desktop_mode_update_available?.slug;
8252 if (fromUpdate) {
8253 return fromUpdate;
8254 }
8255 const file = typeof row.plugin === "string" ? row.plugin : "";
8256 if (file) {
8257 const slash = file.indexOf("/");
8258 if (slash > 0) {
8259 return file.slice(0, slash);
8260 }
8261 }
8262 if (row.textdomain) {
8263 return String(row.textdomain);
8264 }
8265 return "";
8266 }
8267 function readDescription(row) {
8268 const d = row.description;
8269 if (!d) {
8270 return "";
8271 }
8272 if (typeof d === "string") {
8273 return d;
8274 }
8275 return d.rendered || d.raw || "";
8276 }
8277 function formatSize$1(kb) {
8278 if (kb < 1024) {
8279 return sprintf(
8280 /* translators: %d: kilobytes */
8281 __("%d KB", "desktop-mode"),
8282 kb
8283 );
8284 }
8285 return sprintf(
8286 /* translators: %s: megabytes (one decimal) */
8287 __("%s MB", "desktop-mode"),
8288 (kb / 1024).toFixed(1)
8289 );
8290 }
8291 function humanDate(raw) {
8292 const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raw);
8293 if (!m) {
8294 return raw;
8295 }
8296 try {
8297 return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])).toLocaleDateString();
8298 } catch {
8299 return raw;
8300 }
8301 }
8302 function stripHtml$1(html2) {
8303 const tmp = document.createElement("div");
8304 tmp.innerHTML = html2;
8305 return tmp.textContent ?? "";
8306 }
8307 function sanitizeHtml(html2) {
8308 const allowed = /* @__PURE__ */ new Set([
8309 "A",
8310 "ABBR",
8311 "B",
8312 "BLOCKQUOTE",
8313 "BR",
8314 "CODE",
8315 "DD",
8316 "DEL",
8317 "DIV",
8318 "DL",
8319 "DT",
8320 "EM",
8321 "FIGCAPTION",
8322 "FIGURE",
8323 "H1",
8324 "H2",
8325 "H3",
8326 "H4",
8327 "H5",
8328 "H6",
8329 "HR",
8330 "I",
8331 "IMG",
8332 "KBD",
8333 "LI",
8334 "OL",
8335 "P",
8336 "PRE",
8337 "Q",
8338 "S",
8339 "SMALL",
8340 "SPAN",
8341 "STRONG",
8342 "SUB",
8343 "SUP",
8344 "TABLE",
8345 "TBODY",
8346 "TD",
8347 "TFOOT",
8348 "TH",
8349 "THEAD",
8350 "TR",
8351 "U",
8352 "UL"
8353 ]);
8354 const allowedAttrs = /* @__PURE__ */ new Set([
8355 "href",
8356 "src",
8357 "alt",
8358 "title",
8359 "name",
8360 "rel",
8361 "target",
8362 "colspan",
8363 "rowspan"
8364 ]);
8365 const wrap = document.createElement("div");
8366 wrap.innerHTML = html2;
8367 const walker = document.createTreeWalker(wrap, NodeFilter.SHOW_ELEMENT);
8368 const toRemove = [];
8369 let current = walker.currentNode;
8370 while (current) {
8371 const next = walker.nextNode();
8372 if (current === wrap) {
8373 current = next;
8374 continue;
8375 }
8376 if (!allowed.has(current.tagName)) {
8377 toRemove.push(current);
8378 } else {
8379 for (const attr of Array.from(current.attributes)) {
8380 if (!allowedAttrs.has(attr.name.toLowerCase())) {
8381 current.removeAttribute(attr.name);
8382 }
8383 }
8384 if (current.tagName === "A") {
8385 const href = current.getAttribute("href") ?? "";
8386 if (href.toLowerCase().startsWith("javascript:")) {
8387 current.removeAttribute("href");
8388 }
8389 }
8390 if (current.tagName === "IMG") {
8391 const src = current.getAttribute("src") ?? "";
8392 if (src.toLowerCase().startsWith("javascript:")) {
8393 current.removeAttribute("src");
8394 }
8395 }
8396 }
8397 current = next;
8398 }
8399 for (const el of toRemove) {
8400 const text = document.createTextNode(el.textContent ?? "");
8401 el.replaceWith(text);
8402 }
8403 return wrap.innerHTML;
8404 }
8405 const PANEL_STYLES = `
8406 .desktop-mode-plugins__detail {
8407 display: block;
8408 background: var( --wpd-surface-subtle, rgba( 0, 0, 0, 0.025 ) );
8409 border-block-start: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8410 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8411 color: var( --wpd-fg, inherit );
8412 font-size: 13px;
8413 line-height: 1.55;
8414 }
8415
8416 /* Hero */
8417 .desktop-mode-plugins__detail-hero {
8418 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.6 ) );
8419 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8420 }
8421 .desktop-mode-plugins__detail-hero-inner {
8422 display: flex;
8423 align-items: center;
8424 gap: 14px;
8425 padding: 14px 24px;
8426 }
8427 .desktop-mode-plugins__detail-hero-icon {
8428 flex: 0 0 44px;
8429 width: 44px;
8430 height: 44px;
8431 border-radius: 10px;
8432 overflow: hidden;
8433 background: var( --wpd-surface, rgba( 0, 0, 0, 0.04 ) );
8434 box-shadow: 0 0 0 1px var( --wpd-border, rgba( 0, 0, 0, 0.08 ) ) inset;
8435 display: flex;
8436 align-items: center;
8437 justify-content: center;
8438 }
8439 .desktop-mode-plugins__detail-hero-icon img {
8440 width: 100%;
8441 height: 100%;
8442 max-width: 100%;
8443 max-height: 100%;
8444 object-fit: contain;
8445 display: block;
8446 }
8447 .desktop-mode-plugins__detail-hero-icon .dashicons {
8448 font-size: 20px;
8449 width: 20px;
8450 height: 20px;
8451 line-height: 20px;
8452 color: var( --wpd-fg-muted, #888 );
8453 }
8454 .desktop-mode-plugins__detail-hero-text {
8455 flex: 1 1 auto;
8456 min-width: 0;
8457 }
8458 .desktop-mode-plugins__detail-title {
8459 margin: 0;
8460 font-size: 15px;
8461 font-weight: 600;
8462 line-height: 1.25;
8463 letter-spacing: -0.005em;
8464 color: var( --wpd-fg, inherit );
8465 }
8466 .desktop-mode-plugins__detail-byline {
8467 margin: 0;
8468 font-size: 12.5px;
8469 color: var( --wpd-fg-muted, #666 );
8470 }
8471 .desktop-mode-plugins__detail-byline a {
8472 color: inherit;
8473 text-decoration: underline;
8474 text-decoration-color: var( --wpd-border-strong, rgba( 0, 0, 0, 0.25 ) );
8475 }
8476 .desktop-mode-plugins__detail-byline a:hover {
8477 color: var( --wp-admin-theme-color, #2271b1 );
8478 }
8479
8480 /* Tab strip */
8481 .desktop-mode-plugins__detail-tabs-wrap {
8482 padding: 0 24px;
8483 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.6 ) );
8484 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8485 }
8486 .desktop-mode-plugins__detail-tabs {
8487 display: block;
8488 }
8489
8490 /* Body */
8491 .desktop-mode-plugins__detail-body {
8492 padding: 22px 24px 26px;
8493 max-width: 100%;
8494 }
8495
8496 /* Overview chip strip */
8497 .desktop-mode-plugins__detail-chip-strip {
8498 display: flex;
8499 flex-wrap: wrap;
8500 gap: 8px;
8501 align-items: center;
8502 }
8503 .desktop-mode-plugins__detail-stars-pill {
8504 display: inline-flex;
8505 align-items: center;
8506 gap: 6px;
8507 padding: 4px 12px;
8508 border-radius: 999px;
8509 background: rgba( 234, 179, 8, 0.12 );
8510 color: #8a5a00;
8511 font-size: 12px;
8512 font-weight: 600;
8513 }
8514 .desktop-mode-plugins__detail-actions {
8515 padding-top: 4px;
8516 }
8517
8518 /* Sanitized HTML body (description / changelog / FAQ answers) */
8519 .desktop-mode-plugins__detail-html {
8520 color: var( --wpd-fg, inherit );
8521 font-size: 14px;
8522 line-height: 1.65;
8523 max-width: 78ch;
8524 }
8525 .desktop-mode-plugins__detail-html h1,
8526 .desktop-mode-plugins__detail-html h2,
8527 .desktop-mode-plugins__detail-html h3,
8528 .desktop-mode-plugins__detail-html h4 {
8529 margin: 16px 0 6px;
8530 line-height: 1.3;
8531 font-weight: 600;
8532 }
8533 .desktop-mode-plugins__detail-html h1 { font-size: 18px; }
8534 .desktop-mode-plugins__detail-html h2 { font-size: 16px; }
8535 .desktop-mode-plugins__detail-html h3 { font-size: 14.5px; }
8536 .desktop-mode-plugins__detail-html h4 { font-size: 13.5px; }
8537 .desktop-mode-plugins__detail-html p {
8538 margin: 0 0 10px;
8539 }
8540 .desktop-mode-plugins__detail-html ul,
8541 .desktop-mode-plugins__detail-html ol {
8542 margin: 0 0 10px;
8543 padding-inline-start: 22px;
8544 }
8545 .desktop-mode-plugins__detail-html li { margin-bottom: 4px; }
8546 .desktop-mode-plugins__detail-html code,
8547 .desktop-mode-plugins__detail-html pre {
8548 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8549 font-size: 12px;
8550 background: rgba( 0, 0, 0, 0.06 );
8551 border-radius: 4px;
8552 }
8553 .desktop-mode-plugins__detail-html code { padding: 1px 6px; }
8554 .desktop-mode-plugins__detail-html pre {
8555 padding: 10px 12px;
8556 overflow-x: auto;
8557 margin: 0 0 10px;
8558 }
8559 .desktop-mode-plugins__detail-html pre code {
8560 background: transparent;
8561 padding: 0;
8562 }
8563 .desktop-mode-plugins__detail-html a {
8564 color: var( --wp-admin-theme-color, #2271b1 );
8565 }
8566 .desktop-mode-plugins__detail-html img {
8567 display: block;
8568 max-width: 100%;
8569 max-height: 220px;
8570 width: auto;
8571 height: auto;
8572 object-fit: contain;
8573 margin: 8px 0;
8574 border-radius: 6px;
8575 }
8576
8577 /* Details fact cards */
8578 .desktop-mode-plugins__detail-grid {
8579 width: 100%;
8580 }
8581 .desktop-mode-plugins__detail-fact {
8582 min-width: 0;
8583 }
8584 .desktop-mode-plugins__detail-fact-head {
8585 display: flex;
8586 align-items: center;
8587 gap: 8px;
8588 color: var( --wpd-fg-muted, #666 );
8589 font-size: 11px;
8590 font-weight: 600;
8591 letter-spacing: 0.06em;
8592 text-transform: uppercase;
8593 }
8594 .desktop-mode-plugins__detail-fact-head .dashicons {
8595 font-size: 14px;
8596 width: 14px;
8597 height: 14px;
8598 line-height: 14px;
8599 }
8600 .desktop-mode-plugins__detail-fact-value {
8601 font-size: 14px;
8602 color: var( --wpd-fg, inherit );
8603 word-break: break-word;
8604 overflow-wrap: anywhere;
8605 font-weight: 500;
8606 }
8607 .desktop-mode-plugins__detail-fact-value code {
8608 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8609 font-size: 12.5px;
8610 background: rgba( 0, 0, 0, 0.06 );
8611 padding: 2px 7px;
8612 border-radius: 4px;
8613 font-weight: 400;
8614 }
8615 .desktop-mode-plugins__detail-fact-value a {
8616 color: var( --wp-admin-theme-color, #2271b1 );
8617 text-decoration: none;
8618 }
8619 .desktop-mode-plugins__detail-fact-value a:hover {
8620 text-decoration: underline;
8621 }
8622
8623 /* Changelog — version-grouped cards */
8624 .desktop-mode-plugins__detail-changelog {
8625 width: 100%;
8626 }
8627 .desktop-mode-plugins__detail-changelog-entry {
8628 width: 100%;
8629 }
8630 .desktop-mode-plugins__detail-changelog-head {
8631 display: flex;
8632 align-items: center;
8633 gap: 10px;
8634 }
8635 .desktop-mode-plugins__detail-changelog-latest {
8636 font-size: 11px;
8637 font-weight: 600;
8638 letter-spacing: 0.06em;
8639 text-transform: uppercase;
8640 color: var( --wpd-fg-muted, #666 );
8641 }
8642
8643 /* FAQ — accordion */
8644 .desktop-mode-plugins__detail-faq {
8645 width: 100%;
8646 }
8647 .desktop-mode-plugins__detail-faq-item {
8648 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.7 ) );
8649 border: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8650 border-radius: 12px;
8651 overflow: hidden;
8652 transition: box-shadow 160ms ease, border-color 160ms ease;
8653 }
8654 .desktop-mode-plugins__detail-faq-item[open] {
8655 border-color: var( --wp-admin-theme-color, #2271b1 );
8656 box-shadow: 0 4px 14px rgba( 0, 0, 0, 0.06 );
8657 }
8658 .desktop-mode-plugins__detail-faq-q {
8659 display: flex;
8660 align-items: center;
8661 gap: 10px;
8662 padding: 14px 16px;
8663 cursor: pointer;
8664 list-style: none;
8665 user-select: none;
8666 }
8667 .desktop-mode-plugins__detail-faq-q::-webkit-details-marker {
8668 display: none;
8669 }
8670 .desktop-mode-plugins__detail-faq-q:hover {
8671 background: rgba( 0, 0, 0, 0.025 );
8672 }
8673 .desktop-mode-plugins__detail-faq-q-text {
8674 flex: 1 1 auto;
8675 font-size: 14px;
8676 font-weight: 600;
8677 color: var( --wpd-fg, inherit );
8678 line-height: 1.4;
8679 }
8680 .desktop-mode-plugins__detail-faq-chevron {
8681 flex: 0 0 auto;
8682 width: 24px;
8683 height: 24px;
8684 border-radius: 50%;
8685 display: inline-flex;
8686 align-items: center;
8687 justify-content: center;
8688 background: rgba( 0, 0, 0, 0.05 );
8689 color: var( --wpd-fg-muted, #555 );
8690 transition: transform 200ms cubic-bezier( 0.2, 0.8, 0.2, 1 ), background 160ms ease;
8691 }
8692 .desktop-mode-plugins__detail-faq-item[open] .desktop-mode-plugins__detail-faq-chevron {
8693 transform: rotate( 180deg );
8694 background: var( --wp-admin-theme-color, #2271b1 );
8695 color: #fff;
8696 }
8697 .desktop-mode-plugins__detail-faq-a {
8698 padding: 4px 16px 16px;
8699 border-block-start: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.06 ) );
8700 background: rgba( 0, 0, 0, 0.012 );
8701 }
8702 @media ( prefers-reduced-motion: reduce ) {
8703 .desktop-mode-plugins__detail-faq-chevron,
8704 .desktop-mode-plugins__detail-faq-item {
8705 transition: none;
8706 }
8707 }
8708
8709 /* Reviews */
8710 .desktop-mode-plugins__detail-reviews {
8711 width: 100%;
8712 }
8713 .desktop-mode-plugins__detail-reviews-grid {
8714 width: 100%;
8715 }
8716 .desktop-mode-plugins__detail-reviews-more,
8717 .desktop-mode-plugins__detail-reviews-cta {
8718 display: flex;
8719 justify-content: center;
8720 padding-top: 12px;
8721 }
8722 .desktop-mode-plugins__detail-review {
8723 width: 100%;
8724 height: 100%;
8725 box-sizing: border-box;
8726 }
8727 .desktop-mode-plugins__detail-review-body {
8728 display: -webkit-box;
8729 -webkit-line-clamp: 4;
8730 -webkit-box-orient: vertical;
8731 overflow: hidden;
8732 }
8733 @media ( max-width: 720px ) {
8734 .desktop-mode-plugins__detail-reviews-grid {
8735 grid-template-columns: 1fr !important;
8736 }
8737 }
8738 .desktop-mode-plugins__detail-review-head {
8739 display: flex;
8740 align-items: center;
8741 gap: 10px;
8742 flex-wrap: wrap;
8743 }
8744 .desktop-mode-plugins__detail-review-date {
8745 margin-inline-start: auto;
8746 font-size: 11.5px;
8747 color: var( --wpd-fg-muted, #888 );
8748 }
8749 .desktop-mode-plugins__detail-review-body {
8750 margin: 0;
8751 font-size: 13px;
8752 color: var( --wpd-fg, inherit );
8753 line-height: 1.55;
8754 }
8755 .desktop-mode-plugins__detail-review-link {
8756 font-size: 12px;
8757 font-weight: 600;
8758 color: var( --wp-admin-theme-color, #2271b1 );
8759 text-decoration: none;
8760 }
8761 .desktop-mode-plugins__detail-review-link:hover {
8762 text-decoration: underline;
8763 }
8764
8765 /* Loading block */
8766 .desktop-mode-plugins__detail-loading-block {
8767 display: inline-flex;
8768 align-items: center;
8769 gap: 10px;
8770 padding: 12px 14px;
8771 border-radius: 10px;
8772 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.7 ) );
8773 border: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8774 color: var( --wpd-fg-muted, #666 );
8775 font-size: 13px;
8776 }
8777
8778 @media ( max-width: 720px ) {
8779 .desktop-mode-plugins__detail-grid {
8780 grid-template-columns: 1fr !important;
8781 }
8782 }
8783 `;
8784 const PLUGINS_CHANGED_TOPIC = "desktop-mode.plugin.changed";
8785 const SOURCE = "installed-view";
8786 function toast(message, duration = 3500) {
8787 const api2 = window.wp?.desktop;
8788 if (api2 && typeof api2.showToast === "function") {
8789 api2.showToast({ message, duration });
8790 return;
8791 }
8792 console.log("[plugins-window]", message);
8793 }
8794 async function confirm(opts) {
8795 const api2 = window.wp?.desktop;
8796 if (api2 && typeof api2.confirm === "function") {
8797 return api2.confirm(opts);
8798 }
8799 return Promise.resolve(true);
8800 }
8801 function mountInstalledView(host) {
8802 host.replaceChildren();
8803 const state = {
8804 rows: [],
8805 statusFilter: "",
8806 search: "",
8807 loading: true,
8808 updating: /* @__PURE__ */ new Set(),
8809 autoUpdating: /* @__PURE__ */ new Set()
8810 };
8811 const toolbar = document.createElement("header");
8812 toolbar.className = "desktop-mode-plugins__toolbar";
8813 const left = document.createElement("div");
8814 left.className = "desktop-mode-plugins__toolbar-left";
8815 const statusFilter = document.createElement("wpd-segmented");
8816 statusFilter.setAttribute("value", "");
8817 const statusOptions = [
8818 { value: "", label: __("All", "desktop-mode") },
8819 { value: "active", label: __("Active", "desktop-mode") },
8820 { value: "inactive", label: __("Inactive", "desktop-mode") },
8821 { value: "update", label: __("Update available", "desktop-mode") }
8822 ];
8823 let updateCountBadge = null;
8824 for (const opt of statusOptions) {
8825 const seg = document.createElement("wpd-segment");
8826 seg.setAttribute("value", opt.value);
8827 if (opt.value === "update") {
8828 const label = document.createElement("span");
8829 label.textContent = opt.label;
8830 seg.appendChild(label);
8831 const badge = document.createElement("wpd-badge");
8832 badge.setAttribute("tone", "warning");
8833 badge.setAttribute("no-dot", "");
8834 badge.style.cssText = "margin-inline-start:6px;";
8835 badge.hidden = true;
8836 seg.appendChild(badge);
8837 updateCountBadge = badge;
8838 } else {
8839 seg.textContent = opt.label;
8840 }
8841 statusFilter.appendChild(seg);
8842 }
8843 statusFilter.addEventListener("wpd-pick", (ev) => {
8844 const detail = ev.detail;
8845 state.statusFilter = detail?.value ?? "";
8846 paintTable();
8847 });
8848 const search = document.createElement("wpd-text-field");
8849 search.setAttribute(
8850 "placeholder",
8851 __("Search installed plugins…", "desktop-mode")
8852 );
8853 let searchDebounce;
8854 search.addEventListener("wpd-input-change", (ev) => {
8855 const value = ev.detail?.value ?? "";
8856 window.clearTimeout(searchDebounce);
8857 searchDebounce = window.setTimeout(() => {
8858 state.search = value;
8859 paintTable();
8860 }, 200);
8861 });
8862 left.append(statusFilter, search);
8863 const right = document.createElement("div");
8864 right.className = "desktop-mode-plugins__toolbar-right";
8865 const bulkBar = document.createElement("div");
8866 bulkBar.className = "desktop-mode-plugins__bulk";
8867 bulkBar.hidden = true;
8868 right.appendChild(bulkBar);
8869 const trailing = document.createElement("div");
8870 trailing.className = "desktop-mode-plugins__toolbar-trailing";
8871 const refreshButton = document.createElement("wpd-button");
8872 refreshButton.setAttribute("variant", "ghost");
8873 refreshButton.setAttribute("title", __("Refresh", "desktop-mode"));
8874 refreshButton.innerHTML = '<span class="dashicons dashicons-update" aria-hidden="true"></span>';
8875 refreshButton.addEventListener("click", () => {
8876 void (async () => {
8877 await reload({ force: true });
8878 void refreshFrameworkMenu();
8879 })();
8880 });
8881 trailing.appendChild(refreshButton);
8882 toolbar.append(left, right, trailing);
8883 const tableWrap = document.createElement("div");
8884 tableWrap.className = "desktop-mode-plugins__body";
8885 const table = document.createElement("wpd-table");
8886 table.setAttribute("selectable", "multi");
8887 table.setAttribute("sticky-header", "");
8888 table.setAttribute("sticky-columns", "1");
8889 table.setAttribute("hover", "");
8890 table.setAttribute("striped", "");
8891 table.setAttribute("bordered", "");
8892 table.setAttribute("loading", "");
8893 table.setAttribute("data-installed-rows", "");
8894 const empty = document.createElement("div");
8895 empty.setAttribute("slot", "empty");
8896 empty.className = "desktop-mode-plugins__empty";
8897 empty.innerHTML = '<span class="dashicons dashicons-admin-plugins" aria-hidden="true"></span><p>' + __("No plugins match your filters.", "desktop-mode") + "</p>";
8898 table.appendChild(empty);
8899 const getRowId = (row, index) => row.plugin || String(index);
8900 table.getRowId = getRowId;
8901 table.columns = buildColumns();
8902 table.subTable = (row) => buildInstalledDetail(row);
8903 table.addEventListener("wpd-table-row-click", (ev) => {
8904 const detail = ev.detail;
8905 if (!detail) {
8906 return;
8907 }
8908 if (table.isExpanded(detail.index)) {
8909 table.collapse(detail.index);
8910 } else {
8911 table.expand(detail.index);
8912 }
8913 });
8914 tableWrap.appendChild(table);
8915 host.append(toolbar, tableWrap);
8916 const selectionListener = (ev) => {
8917 const detail = ev.detail;
8918 const ids = detail?.selection ?? [];
8919 paintBulkBar(ids);
8920 };
8921 table.addEventListener("wpd-table-selection-change", selectionListener);
8922 void reload();
8923 function buildColumns() {
8924 const cfg = getConfig();
8925 const cols = [
8926 {
8927 key: "name",
8928 label: __("Plugin", "desktop-mode"),
8929 sortable: true,
8930 sticky: true,
8931 render: (_value, row) => renderNameCell(row)
8932 },
8933 {
8934 key: "status",
8935 label: __("Status", "desktop-mode"),
8936 sortable: true,
8937 render: (_value, row) => renderStatusCell(row)
8938 },
8939 {
8940 key: "version",
8941 label: __("Version", "desktop-mode"),
8942 sortable: true,
8943 render: (_value, row) => renderVersionCell(row)
8944 },
8945 {
8946 key: "author",
8947 label: __("Author", "desktop-mode"),
8948 render: (_value, row) => renderAuthorCell(row)
8949 },
8950 {
8951 key: "desktop_mode_size_kb",
8952 label: __("Size", "desktop-mode"),
8953 align: "end",
8954 sortable: true,
8955 sortValue: (row) => row.desktop_mode_size_kb ?? 0,
8956 render: (_value, row) => formatSize(row.desktop_mode_size_kb ?? null)
8957 }
8958 ];
8959 if (cfg.autoUpdatesEnabled) {
8960 cols.push({
8961 key: "auto_updates",
8962 label: __("Automatic Updates", "desktop-mode"),
8963 sortable: true,
8964 sortValue: (row) => row.desktop_mode_auto_update?.enabled ? 1 : 0,
8965 render: (_value, row) => renderAutoUpdateCell(row)
8966 });
8967 }
8968 cols.push({
8969 key: "_actions",
8970 label: "",
8971 align: "end",
8972 render: (_value, row) => renderActionsCell(row)
8973 });
8974 return cfg.caps.activate || cfg.caps.delete ? cols : cols.slice(0, -1);
8975 }
8976 function renderNameCell(row) {
8977 const wrap = document.createElement("div");
8978 wrap.style.cssText = "display:flex;align-items:center;gap:12px;min-width:0;padding:4px 0;";
8979 const icon = document.createElement("div");
8980 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;";
8981 const url = row.desktop_mode_icon_url;
8982 if (url) {
8983 const img = document.createElement("img");
8984 img.alt = "";
8985 img.loading = "lazy";
8986 img.decoding = "async";
8987 img.style.cssText = "width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;display:block;";
8988 img.src = attachIconFallback(img, url, () => {
8989 icon.replaceChildren(buildFallbackIcon());
8990 });
8991 icon.appendChild(img);
8992 } else {
8993 icon.appendChild(buildFallbackIcon());
8994 }
8995 const text = document.createElement("div");
8996 text.style.cssText = "display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto;line-height:1.35;";
8997 const title = document.createElement("strong");
8998 title.textContent = row.name || row.plugin;
8999 title.style.cssText = "display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
9000 const path = document.createElement("span");
9001 path.textContent = row.plugin;
9002 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;";
9003 text.append(title, path);
9004 wrap.append(icon, text);
9005 return wrap;
9006 }
9007 function buildFallbackIcon() {
9008 const fallback = document.createElement("span");
9009 fallback.className = "dashicons dashicons-admin-plugins";
9010 fallback.setAttribute("aria-hidden", "true");
9011 fallback.style.cssText = "font-size:18px;width:18px;height:18px;line-height:18px;color:#888;";
9012 return fallback;
9013 }
9014 function renderStatusCell(row) {
9015 const badge = document.createElement("span");
9016 const isActive = row.status === "active" || row.status === "active-network";
9017 const dot = isActive ? "#16a34a" : "#9ca3af";
9018 const bg = isActive ? "rgba(22, 163, 74, 0.14)" : "rgba(120, 120, 120, 0.12)";
9019 const fg = isActive ? "#166e37" : "#555";
9020 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};`;
9021 const dotEl = document.createElement("span");
9022 dotEl.style.cssText = `width:6px;height:6px;border-radius:50%;background:${dot};flex:0 0 auto;display:inline-block;`;
9023 badge.appendChild(dotEl);
9024 const label = document.createElement("span");
9025 label.textContent = isActive ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode");
9026 badge.appendChild(label);
9027 return badge;
9028 }
9029 function renderVersionCell(row) {
9030 const wrap = document.createElement("div");
9031 wrap.style.cssText = "display:flex;align-items:center;gap:6px;flex-wrap:wrap;";
9032 const v = document.createElement("span");
9033 v.textContent = row.version ?? "";
9034 wrap.appendChild(v);
9035 const update = row.desktop_mode_update_available;
9036 if (update?.available && update.new_version) {
9037 const badge = document.createElement("span");
9038 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;";
9039 badge.textContent = sprintf(
9040 /* translators: %s: new plugin version */
9041 __("→ %s", "desktop-mode"),
9042 update.new_version
9043 );
9044 wrap.appendChild(badge);
9045 }
9046 return wrap;
9047 }
9048 function renderAuthorCell(row) {
9049 const wrap = document.createElement("span");
9050 wrap.style.cssText = "white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;";
9051 const text = stripHtml(row.author ?? "");
9052 wrap.textContent = text || __("Unknown", "desktop-mode");
9053 return wrap;
9054 }
9055 function renderAutoUpdateCell(row) {
9056 const wrap = document.createElement("div");
9057 wrap.setAttribute("data-noclick", "");
9058 wrap.style.cssText = "display:inline-flex;align-items:center;gap:6px;white-space:nowrap;";
9059 const meta = row.desktop_mode_auto_update;
9060 const forced = meta?.forced ?? null;
9061 if (forced !== null) {
9062 const label2 = document.createElement("span");
9063 label2.style.cssText = "color:var(--wp-desktop-text-muted,#666);";
9064 label2.textContent = forced ? __("Auto-updates enabled", "desktop-mode") : __("Auto-updates disabled", "desktop-mode");
9065 wrap.appendChild(label2);
9066 return wrap;
9067 }
9068 const supported = !!meta?.supported;
9069 if (!supported) {
9070 const placeholder = document.createElement("span");
9071 placeholder.style.cssText = "color:var(--wp-desktop-text-muted,#9ca3af);";
9072 placeholder.textContent = "";
9073 placeholder.title = __(
9074 "This plugin does not check in with WordPress.org, so automatic updates can't be scheduled.",
9075 "desktop-mode"
9076 );
9077 wrap.appendChild(placeholder);
9078 return wrap;
9079 }
9080 const enabled = !!meta?.enabled;
9081 const busy = state.autoUpdating.has(row.plugin);
9082 const link = document.createElement("a");
9083 link.href = "#";
9084 link.setAttribute("role", "button");
9085 link.setAttribute("data-wp-action", enabled ? "disable" : "enable");
9086 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;";
9087 if (busy) {
9088 link.style.opacity = "0.6";
9089 link.style.pointerEvents = "none";
9090 link.setAttribute("aria-busy", "true");
9091 }
9092 const label = document.createElement("span");
9093 if (busy) {
9094 label.textContent = enabled ? __("Disabling…", "desktop-mode") : __("Enabling…", "desktop-mode");
9095 } else {
9096 label.textContent = enabled ? __("Disable auto-updates", "desktop-mode") : __("Enable auto-updates", "desktop-mode");
9097 }
9098 link.appendChild(label);
9099 link.addEventListener("click", (e) => {
9100 e.preventDefault();
9101 e.stopPropagation();
9102 void runToggleAutoUpdate(row);
9103 });
9104 wrap.appendChild(link);
9105 return wrap;
9106 }
9107 function renderActionsCell(row) {
9108 const wrap = document.createElement("div");
9109 wrap.style.cssText = "display:inline-flex;gap:8px;align-items:center;justify-content:flex-end;flex-wrap:nowrap;";
9110 wrap.setAttribute("data-noclick", "");
9111 const can = row.desktop_mode_can_manage ?? {
9112 activate: row.status === "inactive",
9113 deactivate: row.status === "active" || row.status === "active-network",
9114 delete: row.status === "inactive"
9115 };
9116 const update = row.desktop_mode_update_available;
9117 if (getConfig().caps.update && update?.available) {
9118 if (update.package) {
9119 const updating = state.updating.has(row.plugin);
9120 const label = updating ? __("Updating…", "desktop-mode") : sprintf(
9121 /* translators: %s: new plugin version (e.g. "1.4.2") */
9122 __("Update to %s", "desktop-mode"),
9123 update.new_version ?? ""
9124 );
9125 const btn = button2(label, "primary");
9126 if (updating) {
9127 btn.setAttribute("disabled", "");
9128 btn.setAttribute("aria-busy", "true");
9129 }
9130 btn.addEventListener("click", (e) => {
9131 e.stopPropagation();
9132 void runUpdate(row);
9133 });
9134 wrap.appendChild(btn);
9135 } else {
9136 const hint = document.createElement("span");
9137 hint.style.cssText = "font-size:0.78em;color:var(--wp-desktop-text-muted,#666);";
9138 hint.textContent = __("Auto-update unavailable", "desktop-mode");
9139 hint.title = __(
9140 "This plugin does not ship a wp.org download package. Update it manually from its source.",
9141 "desktop-mode"
9142 );
9143 wrap.appendChild(hint);
9144 }
9145 }
9146 if (can.activate) {
9147 const btn = button2(__("Activate", "desktop-mode"), "primary");
9148 btn.addEventListener("click", (e) => {
9149 e.stopPropagation();
9150 void runActivate(row);
9151 });
9152 wrap.appendChild(btn);
9153 } else if (can.deactivate) {
9154 const btn = button2(__("Deactivate", "desktop-mode"), "secondary");
9155 btn.addEventListener("click", (e) => {
9156 e.stopPropagation();
9157 void runDeactivate(row);
9158 });
9159 wrap.appendChild(btn);
9160 }
9161 if (can.delete) {
9162 const btn = button2(__("Delete", "desktop-mode"), "danger");
9163 btn.addEventListener("click", (e) => {
9164 e.stopPropagation();
9165 void runDelete(row);
9166 });
9167 wrap.appendChild(btn);
9168 }
9169 return wrap;
9170 }
9171 function button2(label, variant) {
9172 const b = document.createElement("wpd-button");
9173 b.setAttribute("variant", variant);
9174 b.setAttribute("size", "small");
9175 b.textContent = label;
9176 return b;
9177 }
9178 function paintBulkBar(ids) {
9179 bulkBar.replaceChildren();
9180 if (ids.length === 0) {
9181 bulkBar.hidden = true;
9182 return;
9183 }
9184 bulkBar.hidden = false;
9185 const count = document.createElement("span");
9186 count.className = "desktop-mode-plugins__bulk-count";
9187 count.textContent = sprintf(
9188 /* translators: %d: number of selected plugins */
9189 __("%d selected", "desktop-mode"),
9190 ids.length
9191 );
9192 bulkBar.appendChild(count);
9193 const cfg = getConfig();
9194 const selected = state.rows.filter((r) => ids.includes(r.plugin));
9195 if (cfg.caps.update) {
9196 const updatable = selected.filter(
9197 (r) => !!r.desktop_mode_update_available?.available && !!r.desktop_mode_update_available.package
9198 );
9199 if (updatable.length > 0) {
9200 const btn = button2(
9201 sprintf(
9202 /* translators: %d: number of plugins with pending updates */
9203 __("Update %d", "desktop-mode"),
9204 updatable.length
9205 ),
9206 "primary"
9207 );
9208 btn.addEventListener("click", () => {
9209 void runBulk(updatable, "update");
9210 });
9211 bulkBar.appendChild(btn);
9212 }
9213 }
9214 if (cfg.caps.activate) {
9215 const activatable = selected.filter((r) => r.status === "inactive");
9216 if (activatable.length > 0) {
9217 const btn = button2(__("Activate", "desktop-mode"), "primary");
9218 btn.addEventListener("click", () => {
9219 void runBulk(activatable, "activate");
9220 });
9221 bulkBar.appendChild(btn);
9222 }
9223 const deactivatable = selected.filter(
9224 (r) => r.status === "active" || r.status === "active-network"
9225 );
9226 if (deactivatable.length > 0) {
9227 const btn = button2(__("Deactivate", "desktop-mode"), "secondary");
9228 btn.addEventListener("click", () => {
9229 void runBulk(deactivatable, "deactivate");
9230 });
9231 bulkBar.appendChild(btn);
9232 }
9233 }
9234 if (cfg.caps.delete) {
9235 const deletable = selected.filter((r) => r.status === "inactive");
9236 if (deletable.length > 0) {
9237 const btn = button2(__("Delete", "desktop-mode"), "danger");
9238 btn.addEventListener("click", () => {
9239 void runBulk(deletable, "delete");
9240 });
9241 bulkBar.appendChild(btn);
9242 }
9243 }
9244 }
9245 async function reload(opts = {}) {
9246 state.loading = true;
9247 table.setAttribute("loading", "");
9248 try {
9249 state.rows = await fetchInstalledPlugins(opts);
9250 } catch (err) {
9251 toast(
9252 sprintf(
9253 /* translators: %s: error message */
9254 __("Could not load plugins: %s", "desktop-mode"),
9255 describe(err)
9256 ),
9257 6e3
9258 );
9259 state.rows = [];
9260 }
9261 state.loading = false;
9262 paintTable();
9263 }
9264 function paintTable() {
9265 if (state.loading) {
9266 table.setAttribute("loading", "");
9267 } else {
9268 table.removeAttribute("loading");
9269 }
9270 table.data = filterRows(state.rows);
9271 paintUpdateCount();
9272 }
9273 function paintUpdateCount() {
9274 if (!updateCountBadge) {
9275 return;
9276 }
9277 const count = state.rows.filter(
9278 (r) => !!r.desktop_mode_update_available?.available
9279 ).length;
9280 if (count > 0) {
9281 updateCountBadge.textContent = String(count);
9282 updateCountBadge.hidden = false;
9283 } else {
9284 updateCountBadge.hidden = true;
9285 updateCountBadge.textContent = "";
9286 }
9287 }
9288 function filterRows(rows) {
9289 const q = state.search.trim().toLowerCase();
9290 const status = state.statusFilter;
9291 return rows.filter((row) => {
9292 if (status === "active") {
9293 if (row.status !== "active" && row.status !== "active-network") {
9294 return false;
9295 }
9296 } else if (status === "inactive") {
9297 if (row.status !== "inactive") {
9298 return false;
9299 }
9300 } else if (status === "update") {
9301 if (!row.desktop_mode_update_available?.available) {
9302 return false;
9303 }
9304 }
9305 if (q !== "") {
9306 const haystack = `${row.name ?? ""} ${row.plugin} ${stripHtml(row.author ?? "")}`.toLowerCase();
9307 if (!haystack.includes(q)) {
9308 return false;
9309 }
9310 }
9311 return true;
9312 });
9313 }
9314 async function runActivate(row) {
9315 const previous = row.status;
9316 applyStatusOptimistic(row, "active");
9317 try {
9318 const updated = await activateInstalledPlugin(row);
9319 mergeRow(updated);
9320 toast(
9321 sprintf(
9322 /* translators: %s: plugin name */
9323 __("%s activated.", "desktop-mode"),
9324 row.name || row.plugin
9325 )
9326 );
9327 broadcast(PLUGINS_CHANGED_TOPIC, {
9328 source: SOURCE,
9329 plugin: row.plugin,
9330 action: "activate"
9331 });
9332 void refreshFrameworkMenu();
9333 } catch (err) {
9334 applyStatusOptimistic(row, previous);
9335 toast(
9336 sprintf(
9337 /* translators: %s: error message */
9338 __("Activation failed: %s", "desktop-mode"),
9339 describe(err)
9340 ),
9341 6e3
9342 );
9343 }
9344 }
9345 async function runDeactivate(row) {
9346 const previous = row.status;
9347 applyStatusOptimistic(row, "inactive");
9348 try {
9349 const updated = await deactivateInstalledPlugin(row);
9350 mergeRow(updated);
9351 if (isDesktopModeSelf(row.plugin)) {
9352 toast(
9353 __(
9354 "Desktop Mode deactivated. Reloading…",
9355 "desktop-mode"
9356 ),
9357 2e3
9358 );
9359 reloadOutOfDesktopMode();
9360 return;
9361 }
9362 toast(
9363 sprintf(
9364 /* translators: %s: plugin name */
9365 __("%s deactivated.", "desktop-mode"),
9366 row.name || row.plugin
9367 )
9368 );
9369 broadcast(PLUGINS_CHANGED_TOPIC, {
9370 source: SOURCE,
9371 plugin: row.plugin,
9372 action: "deactivate"
9373 });
9374 void refreshFrameworkMenu();
9375 } catch (err) {
9376 applyStatusOptimistic(row, previous);
9377 toast(
9378 sprintf(
9379 /* translators: %s: error message */
9380 __("Deactivation failed: %s", "desktop-mode"),
9381 describe(err)
9382 ),
9383 6e3
9384 );
9385 }
9386 }
9387 async function runDelete(row) {
9388 const ok = await confirm({
9389 title: __("Delete plugin?", "desktop-mode"),
9390 message: sprintf(
9391 /* translators: %s: plugin name */
9392 __(
9393 "Permanently delete %s? Its files will be removed from disk. This cannot be undone.",
9394 "desktop-mode"
9395 ),
9396 row.name || row.plugin
9397 ),
9398 confirmLabel: __("Delete", "desktop-mode"),
9399 danger: true
9400 });
9401 if (!ok) {
9402 return;
9403 }
9404 try {
9405 await deleteInstalledPlugin(row);
9406 state.rows = state.rows.filter((r) => r.plugin !== row.plugin);
9407 paintTable();
9408 if (isDesktopModeSelf(row.plugin)) {
9409 toast(
9410 __(
9411 "Desktop Mode deleted. Reloading…",
9412 "desktop-mode"
9413 ),
9414 2e3
9415 );
9416 reloadOutOfDesktopMode();
9417 return;
9418 }
9419 toast(
9420 sprintf(
9421 /* translators: %s: plugin name */
9422 __("%s deleted.", "desktop-mode"),
9423 row.name || row.plugin
9424 )
9425 );
9426 broadcast(PLUGINS_CHANGED_TOPIC, {
9427 source: SOURCE,
9428 plugin: row.plugin,
9429 action: "delete"
9430 });
9431 void refreshFrameworkMenu();
9432 } catch (err) {
9433 toast(
9434 sprintf(
9435 /* translators: %s: error message */
9436 __("Delete failed: %s", "desktop-mode"),
9437 describe(err)
9438 ),
9439 6e3
9440 );
9441 }
9442 }
9443 async function runUpdate(row) {
9444 if (state.updating.has(row.plugin)) {
9445 return;
9446 }
9447 state.updating.add(row.plugin);
9448 paintTable();
9449 try {
9450 const result = await enqueueUpdateJob(() => updateInstalledPlugin(row));
9451 mergeRow({
9452 ...row,
9453 version: result.newVersion,
9454 desktop_mode_update_available: {
9455 available: false,
9456 new_version: null,
9457 package: "",
9458 slug: row.desktop_mode_update_available?.slug ?? ""
9459 }
9460 });
9461 toast(
9462 sprintf(
9463 /* translators: 1: plugin name, 2: new version */
9464 __("%1$s updated to %2$s.", "desktop-mode"),
9465 row.name || row.plugin,
9466 result.newVersion
9467 )
9468 );
9469 broadcast(PLUGINS_CHANGED_TOPIC, {
9470 source: SOURCE,
9471 plugin: row.plugin,
9472 action: "update"
9473 });
9474 void refreshFrameworkMenu();
9475 } catch (err) {
9476 const errCode = err?.code;
9477 const errMessage = err?.message;
9478 const coreUpToDateMessage = window.wp?.i18n?.__?.("The plugin is at the latest version.");
9479 const isUpToDate = errCode === "up_to_date" || !!coreUpToDateMessage && errMessage === coreUpToDateMessage;
9480 if (isUpToDate) {
9481 mergeRow({
9482 ...row,
9483 desktop_mode_update_available: {
9484 available: false,
9485 new_version: null,
9486 package: "",
9487 slug: row.desktop_mode_update_available?.slug ?? ""
9488 }
9489 });
9490 toast(
9491 sprintf(
9492 /* translators: %s: plugin name */
9493 __("%s is already up to date.", "desktop-mode"),
9494 row.name || row.plugin
9495 )
9496 );
9497 broadcast(PLUGINS_CHANGED_TOPIC, {
9498 source: SOURCE,
9499 plugin: row.plugin,
9500 action: "update"
9501 });
9502 } else {
9503 toast(
9504 sprintf(
9505 /* translators: 1: plugin name, 2: error message */
9506 __("Update of %1$s failed: %2$s", "desktop-mode"),
9507 row.name || row.plugin,
9508 describe(err)
9509 ),
9510 6e3
9511 );
9512 void reload();
9513 }
9514 void refreshFrameworkMenu();
9515 } finally {
9516 state.updating.delete(row.plugin);
9517 paintTable();
9518 }
9519 }
9520 async function runToggleAutoUpdate(row) {
9521 if (state.autoUpdating.has(row.plugin)) {
9522 return;
9523 }
9524 const meta = row.desktop_mode_auto_update;
9525 if (!meta || meta.forced !== null || !meta.supported) {
9526 return;
9527 }
9528 const wasEnabled = meta.enabled;
9529 const nextState = wasEnabled ? "disable" : "enable";
9530 state.autoUpdating.add(row.plugin);
9531 paintTable();
9532 try {
9533 await toggleAutoUpdate(row, nextState);
9534 mergeRow({
9535 ...row,
9536 desktop_mode_auto_update: {
9537 ...meta,
9538 enabled: !wasEnabled
9539 }
9540 });
9541 toast(
9542 wasEnabled ? sprintf(
9543 /* translators: %s: plugin name */
9544 __("Auto-updates disabled for %s.", "desktop-mode"),
9545 row.name || row.plugin
9546 ) : sprintf(
9547 /* translators: %s: plugin name */
9548 __("Auto-updates enabled for %s.", "desktop-mode"),
9549 row.name || row.plugin
9550 )
9551 );
9552 broadcast(PLUGINS_CHANGED_TOPIC, {
9553 source: SOURCE,
9554 plugin: row.plugin,
9555 action: "auto-update"
9556 });
9557 } catch (err) {
9558 toast(
9559 sprintf(
9560 /* translators: 1: plugin name, 2: error message */
9561 __(
9562 "Could not toggle auto-updates for %1$s: %2$s",
9563 "desktop-mode"
9564 ),
9565 row.name || row.plugin,
9566 describe(err)
9567 ),
9568 6e3
9569 );
9570 } finally {
9571 state.autoUpdating.delete(row.plugin);
9572 paintTable();
9573 }
9574 }
9575 async function runBulk(rows, action) {
9576 if (rows.length === 0) {
9577 return;
9578 }
9579 if (action === "delete") {
9580 const ok = await confirm({
9581 title: __("Delete selected plugins?", "desktop-mode"),
9582 message: sprintf(
9583 /* translators: %d: number of plugins */
9584 __(
9585 "Permanently delete %d plugin(s)? Their files will be removed from disk. This cannot be undone.",
9586 "desktop-mode"
9587 ),
9588 rows.length
9589 ),
9590 confirmLabel: __("Delete", "desktop-mode"),
9591 danger: true
9592 });
9593 if (!ok) {
9594 return;
9595 }
9596 }
9597 let succeeded = 0;
9598 let selfMutated = false;
9599 const failures = [];
9600 for (const row of rows) {
9601 try {
9602 if (action === "activate") {
9603 mergeRow(await activateInstalledPlugin(row));
9604 } else if (action === "deactivate") {
9605 mergeRow(await deactivateInstalledPlugin(row));
9606 } else if (action === "delete") {
9607 await deleteInstalledPlugin(row);
9608 state.rows = state.rows.filter((r) => r.plugin !== row.plugin);
9609 } else if (action === "update") {
9610 state.updating.add(row.plugin);
9611 paintTable();
9612 try {
9613 const result = await enqueueUpdateJob(
9614 () => updateInstalledPlugin(row)
9615 );
9616 mergeRow({
9617 ...row,
9618 version: result.newVersion,
9619 desktop_mode_update_available: {
9620 available: false,
9621 new_version: null,
9622 package: "",
9623 slug: row.desktop_mode_update_available?.slug ?? ""
9624 }
9625 });
9626 } finally {
9627 state.updating.delete(row.plugin);
9628 }
9629 }
9630 if ((action === "deactivate" || action === "delete") && isDesktopModeSelf(row.plugin)) {
9631 selfMutated = true;
9632 }
9633 succeeded++;
9634 } catch (err) {
9635 failures.push({ row, err });
9636 }
9637 }
9638 paintTable();
9639 table.clearSelection();
9640 if (selfMutated) {
9641 toast(
9642 action === "delete" ? __("Desktop Mode deleted. Reloading…", "desktop-mode") : __("Desktop Mode deactivated. Reloading…", "desktop-mode"),
9643 2e3
9644 );
9645 reloadOutOfDesktopMode();
9646 return;
9647 }
9648 if (succeeded > 0) {
9649 broadcast(PLUGINS_CHANGED_TOPIC, {
9650 source: SOURCE,
9651 action: "bulk"
9652 });
9653 }
9654 void refreshFrameworkMenu();
9655 let noun = "";
9656 if (action === "delete") {
9657 noun = __("deleted", "desktop-mode");
9658 } else if (action === "activate") {
9659 noun = __("activated", "desktop-mode");
9660 } else if (action === "update") {
9661 noun = __("updated", "desktop-mode");
9662 } else {
9663 noun = __("deactivated", "desktop-mode");
9664 }
9665 const summary = failures.length === 0 ? sprintf(
9666 /* translators: 1: count, 2: action verb (activated, deactivated, deleted) */
9667 __("%1$d plugin(s) %2$s.", "desktop-mode"),
9668 succeeded,
9669 noun
9670 ) : sprintf(
9671 /* translators: 1: success count, 2: failure count, 3: action verb */
9672 __("%1$d %3$s, %2$d failed.", "desktop-mode"),
9673 succeeded,
9674 failures.length,
9675 noun
9676 );
9677 toast(summary, 5e3);
9678 }
9679 function applyStatusOptimistic(row, next) {
9680 row.status = next;
9681 paintTable();
9682 }
9683 function mergeRow(updated) {
9684 const idx = state.rows.findIndex((r) => r.plugin === updated.plugin);
9685 if (idx >= 0) {
9686 state.rows[idx] = { ...state.rows[idx], ...updated };
9687 } else {
9688 state.rows.push(updated);
9689 }
9690 paintTable();
9691 }
9692 const unsubscribePluginsChanged = subscribe(
9693 PLUGINS_CHANGED_TOPIC,
9694 (payload) => {
9695 if (payload?.source === SOURCE) {
9696 return;
9697 }
9698 void reload();
9699 }
9700 );
9701 return () => {
9702 unsubscribePluginsChanged();
9703 table.removeEventListener("wpd-table-selection-change", selectionListener);
9704 host.replaceChildren();
9705 };
9706 }
9707 function formatSize(kb) {
9708 if (kb === null || kb === void 0) {
9709 return "";
9710 }
9711 if (kb < 1024) {
9712 return sprintf(
9713 /* translators: %d: size in kilobytes */
9714 __("%d KB", "desktop-mode"),
9715 kb
9716 );
9717 }
9718 const mb = kb / 1024;
9719 return sprintf(
9720 /* translators: %s: size in megabytes (one decimal) */
9721 __("%s MB", "desktop-mode"),
9722 mb.toFixed(1)
9723 );
9724 }
9725 function stripHtml(html2) {
9726 const tmp = document.createElement("div");
9727 tmp.innerHTML = html2;
9728 return tmp.textContent ?? "";
9729 }
9730 function describe(err) {
9731 if (err instanceof Error) {
9732 return err.message;
9733 }
9734 return String(err);
9735 }
9736 const _initial = {
9737 tab: null,
9738 requestedAt: 0
9739 };
9740 let _store = null;
9741 function getStore() {
9742 if (_store) {
9743 return _store;
9744 }
9745 const w = window;
9746 const factory = w.wp?.desktop?.createSharedStore;
9747 if (typeof factory !== "function") {
9748 return null;
9749 }
9750 _store = factory(
9751 "desktop-mode/plugins-window/tab-target",
9752 () => ({ ..._initial })
9753 );
9754 return _store;
9755 }
9756 function consumePluginsWindowTab() {
9757 const store = getStore();
9758 if (store) {
9759 const tab = store.state.tab;
9760 if (tab !== null) {
9761 store.state.tab = null;
9762 store.state.requestedAt = 0;
9763 store.notify();
9764 }
9765 return tab;
9766 }
9767 const w = window;
9768 const prev = w._wpdPluginsWindowTab;
9769 if (prev) {
9770 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
9771 return prev.tab;
9772 }
9773 return null;
9774 }
9775 function subscribePluginsWindowTab(cb) {
9776 const store = getStore();
9777 if (!store) {
9778 return () => {
9779 };
9780 }
9781 return store.subscribe((state) => cb({ ...state }));
9782 }
9783 function renderPluginsWindow(body) {
9784 const root = body.querySelector(
9785 "[data-desktop-mode-plugins-root]"
9786 );
9787 if (!root) {
9788 body.innerHTML = '<p style="padding:20px;color:var(--wpd-fg-muted,#666);">' + __("Plugins window template missing.", "desktop-mode") + "</p>";
9789 return;
9790 }
9791 const config = getConfig();
9792 const tabs = root.querySelector(
9793 "[data-desktop-mode-plugins-tabs]"
9794 );
9795 const installedHost = root.querySelector(
9796 "[data-desktop-mode-plugins-installed-host]"
9797 );
9798 let installedTeardown = null;
9799 if (installedHost) {
9800 if (config.caps.activate) {
9801 installedTeardown = mountInstalledView(installedHost);
9802 } else {
9803 installedHost.replaceChildren();
9804 const msg = document.createElement("p");
9805 msg.style.padding = "20px";
9806 msg.style.color = "var(--wpd-fg-muted, #666)";
9807 msg.textContent = __(
9808 "You do not have permission to manage plugins.",
9809 "desktop-mode"
9810 );
9811 installedHost.appendChild(msg);
9812 }
9813 }
9814 const browseHost = root.querySelector(
9815 "[data-desktop-mode-plugins-browse-host]"
9816 );
9817 const flyout = root.querySelector(
9818 "[data-desktop-mode-plugins-flyout]"
9819 );
9820 let browseTeardown = null;
9821 if (browseHost && config.caps.install) {
9822 browseTeardown = mountBrowseView(browseHost, flyout, body);
9823 }
9824 const featuredHost = root.querySelector(
9825 "[data-desktop-mode-plugins-featured-host]"
9826 );
9827 let featuredTeardown = null;
9828 if (featuredHost && config.caps.install) {
9829 featuredTeardown = mountFeaturedView(featuredHost, flyout);
9830 }
9831 const applyTab = (tab) => {
9832 if (!tabs) {
9833 return;
9834 }
9835 if ((tab === "browse" || tab === "featured") && !config.caps.install) {
9836 tabs.setAttribute("value", "installed");
9837 return;
9838 }
9839 tabs.setAttribute("value", tab);
9840 };
9841 const initialTab = consumePluginsWindowTab();
9842 if (initialTab) {
9843 applyTab(initialTab);
9844 }
9845 const unsubscribeTab = subscribePluginsWindowTab((state) => {
9846 if (state.tab) {
9847 applyTab(state.tab);
9848 }
9849 });
9850 const onClosed = (ev) => {
9851 const detail = ev.detail;
9852 if (detail?.windowId !== "desktop-mode-plugins") {
9853 return;
9854 }
9855 document.removeEventListener("desktop-mode-window-closed", onClosed);
9856 unsubscribeTab();
9857 if (installedTeardown) {
9858 installedTeardown();
9859 installedTeardown = null;
9860 }
9861 if (browseTeardown) {
9862 browseTeardown();
9863 browseTeardown = null;
9864 }
9865 if (featuredTeardown) {
9866 featuredTeardown();
9867 featuredTeardown = null;
9868 }
9869 };
9870 document.addEventListener("desktop-mode-window-closed", onClosed);
9871 void maybeShowIntro(config);
9872 }
9873 let _introShown = false;
9874 async function maybeShowIntro(config) {
9875 if (_introShown || config.introSeen) {
9876 return;
9877 }
9878 _introShown = true;
9879 try {
9880 const { showPluginsIntroDialog: showPluginsIntroDialog2 } = await Promise.resolve().then(() => introDialog);
9881 const result = await showPluginsIntroDialog2();
9882 if (result === "cancel") {
9883 _introShown = false;
9884 return;
9885 }
9886 void markIntroSeen(config);
9887 if (result === "settings") {
9888 openOsSettingsFeatures();
9889 }
9890 } catch {
9891 _introShown = false;
9892 }
9893 }
9894 async function markIntroSeen(config) {
9895 if (!config.introUrl) {
9896 return;
9897 }
9898 try {
9899 await trackedFetch(
9900 config.introUrl,
9901 {
9902 method: "POST",
9903 credentials: "same-origin",
9904 headers: {
9905 "Content-Type": "application/json",
9906 "X-WP-Nonce": config.restNonce
9907 },
9908 body: JSON.stringify({ slug: "plugins" })
9909 },
9910 {
9911 windowId: "desktop-mode-plugins",
9912 source: "plugins-window/intro"
9913 }
9914 );
9915 config.introSeen = true;
9916 } catch {
9917 }
9918 }
9919 function openOsSettingsFeatures() {
9920 const api2 = window.wp?.desktop;
9921 if (typeof api2?.openOsSettings === "function") {
9922 api2.openOsSettings("features");
9923 }
9924 }
9925 const registry = window.desktopModeNativeWindows ?? (window.desktopModeNativeWindows = {});
9926 registry["desktop-mode-plugins"] = (body) => {
9927 renderPluginsWindow(body);
9928 };
9929 async function showPluginsIntroDialog() {
9930 return new Promise((resolve) => {
9931 const backdrop = document.createElement("div");
9932 backdrop.className = "desktop-mode-plugins-intro__backdrop";
9933 backdrop.setAttribute("role", "presentation");
9934 Object.assign(backdrop.style, {
9935 position: "fixed",
9936 inset: "0",
9937 background: "color-mix(in srgb, var(--wp-admin-theme-color, #1d2327) 60%, transparent)",
9938 backdropFilter: "blur(2px)",
9939 WebkitBackdropFilter: "blur(2px)",
9940 zIndex: "100000",
9941 display: "flex",
9942 alignItems: "center",
9943 justifyContent: "center",
9944 padding: "24px"
9945 });
9946 const dialog = document.createElement("div");
9947 dialog.setAttribute("role", "dialog");
9948 dialog.setAttribute("aria-modal", "true");
9949 dialog.setAttribute("aria-labelledby", "desktop-mode-plugins-intro-title");
9950 dialog.className = "desktop-mode-plugins-intro";
9951 Object.assign(dialog.style, {
9952 background: "var(--wp-admin-theme-bg, #fff)",
9953 color: "var(--wp-admin-theme-fg, #1d2327)",
9954 borderRadius: "14px",
9955 boxShadow: "0 24px 60px rgba(0,0,0,.28)",
9956 maxWidth: "560px",
9957 width: "100%",
9958 maxHeight: "90vh",
9959 overflow: "auto",
9960 padding: "28px 32px 24px",
9961 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
9962 });
9963 dialog.innerHTML = renderDialogMarkup();
9964 backdrop.appendChild(dialog);
9965 document.body.appendChild(backdrop);
9966 const primaryBtn = dialog.querySelector(
9967 '[data-action="confirm"]'
9968 );
9969 const settingsBtn = dialog.querySelector(
9970 '[data-action="settings"]'
9971 );
9972 primaryBtn?.focus();
9973 let resolved = false;
9974 const cleanup = (result) => {
9975 if (resolved) {
9976 return;
9977 }
9978 resolved = true;
9979 document.removeEventListener("keydown", onKey, true);
9980 backdrop.remove();
9981 resolve(result);
9982 };
9983 const onKey = (e) => {
9984 if (e.key === "Escape") {
9985 e.preventDefault();
9986 cleanup("cancel");
9987 }
9988 };
9989 document.addEventListener("keydown", onKey, true);
9990 backdrop.addEventListener("click", (e) => {
9991 if (e.target === backdrop) {
9992 cleanup("cancel");
9993 }
9994 });
9995 primaryBtn?.addEventListener("click", () => cleanup("confirm"));
9996 settingsBtn?.addEventListener("click", () => cleanup("settings"));
9997 });
9998 }
9999 function renderDialogMarkup() {
10000 const title = __("Welcome to the new Plugins window", "desktop-mode");
10001 const lede = __(
10002 "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.",
10003 "desktop-mode"
10004 );
10005 const highlights = [
10006 __(
10007 "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.",
10008 "desktop-mode"
10009 ),
10010 __(
10011 "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.",
10012 "desktop-mode"
10013 ),
10014 __(
10015 "The detail flyout shows screenshots, the ratings histogram, recent reviews, the changelog and FAQ — all without leaving the window.",
10016 "desktop-mode"
10017 ),
10018 __(
10019 "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.",
10020 "desktop-mode"
10021 ),
10022 __(
10023 'The dock repaints LIVE after every install / activate / deactivate / delete. No reload, no stale tile, no "wait, did that work?".',
10024 "desktop-mode"
10025 ),
10026 __(
10027 "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.",
10028 "desktop-mode"
10029 )
10030 ];
10031 const li = (arr) => arr.map(
10032 (s) => `<li><span class="dot" aria-hidden="true"></span>${escapeHtml(
10033 s
10034 )}</li>`
10035 ).join("");
10036 return `
10037 <style>
10038 .desktop-mode-plugins-intro h2 {
10039 margin: 0 0 8px;
10040 font-size: 22px;
10041 font-weight: 600;
10042 letter-spacing: -0.01em;
10043 }
10044 .desktop-mode-plugins-intro p.lede {
10045 margin: 0 0 20px;
10046 color: var(--wp-admin-theme-fg-muted, #50575e);
10047 font-size: 14px;
10048 line-height: 1.5;
10049 }
10050 .desktop-mode-plugins-intro__list {
10051 list-style: none;
10052 margin: 0 0 22px;
10053 padding: 0;
10054 font-size: 14px;
10055 line-height: 1.5;
10056 }
10057 .desktop-mode-plugins-intro__list li {
10058 display: flex;
10059 align-items: flex-start;
10060 gap: 10px;
10061 padding: 6px 0;
10062 }
10063 .desktop-mode-plugins-intro__list .dot {
10064 flex: 0 0 auto;
10065 width: 6px;
10066 height: 6px;
10067 margin-top: 9px;
10068 border-radius: 50%;
10069 background: var(--wp-admin-theme-color, #2271b1);
10070 }
10071 .desktop-mode-plugins-intro__footer {
10072 display: flex;
10073 justify-content: flex-end;
10074 gap: 8px;
10075 margin-top: 8px;
10076 }
10077 .desktop-mode-plugins-intro__footer button {
10078 appearance: none;
10079 border: 1px solid var(--wp-admin-theme-border, #dcdcde);
10080 background: var(--wp-admin-theme-bg, #fff);
10081 color: inherit;
10082 padding: 8px 14px;
10083 border-radius: 6px;
10084 font-size: 13px;
10085 cursor: pointer;
10086 }
10087 .desktop-mode-plugins-intro__footer button.primary {
10088 border-color: var(--wp-admin-theme-color, #2271b1);
10089 background: var(--wp-admin-theme-color, #2271b1);
10090 color: #fff;
10091 font-weight: 500;
10092 }
10093 .desktop-mode-plugins-intro__footer button:hover {
10094 filter: brightness(1.05);
10095 }
10096 .desktop-mode-plugins-intro__footer button:focus-visible {
10097 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
10098 outline-offset: 2px;
10099 }
10100 </style>
10101 <h2 id="desktop-mode-plugins-intro-title">${escapeHtml(title)}</h2>
10102 <p class="lede">${escapeHtml(lede)}</p>
10103 <ul class="desktop-mode-plugins-intro__list">${li(highlights)}</ul>
10104 <div class="desktop-mode-plugins-intro__footer">
10105 <button type="button" data-action="settings">${escapeHtml(
10106 __("Take me to settings", "desktop-mode")
10107 )}</button>
10108 <button type="button" class="primary" data-action="confirm">${escapeHtml(
10109 __("Got it", "desktop-mode")
10110 )}</button>
10111 </div>
10112 `;
10113 }
10114 function escapeHtml(s) {
10115 const t = document.createElement("div");
10116 t.textContent = s;
10117 return t.innerHTML;
10118 }
10119 const introDialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10120 __proto__: null,
10121 showPluginsIntroDialog
10122 }, Symbol.toStringTag, { value: "Module" }));
10123 })();
10124