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

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

10,136 lines 351.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function() {
2 "use strict";
3 const 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.5.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.6.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 previously-focused element as the trigger for restore-on-close, traps focus while open, and dismisses on Escape / pointerdown-outside / `[data-flyout-close]` click / imperative `open`-removal — all firing one `wpd-flyout-dismiss` event with a `reason` discriminator.",
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.5.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.5.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.5.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 cap.querySelectorAll("a").forEach((a) => {
4041 a.setAttribute("target", "_blank");
4042 a.setAttribute("rel", "noopener nofollow");
4043 });
4044 fig.appendChild(cap);
4045 }
4046 wrap.appendChild(fig);
4047 }
4048 return wrap;
4049 }
4050 function buildRatingsHistogram(info) {
4051 const wrap = document.createElement("div");
4052 wrap.className = "desktop-mode-plugins__histogram";
4053 const ratings = info.ratings ?? {};
4054 const total = Object.values(ratings).reduce(
4055 (a, b) => a + (typeof b === "number" ? b : 0),
4056 0
4057 );
4058 if (total === 0) {
4059 const empty = document.createElement("p");
4060 empty.className = "desktop-mode-plugins__empty-line";
4061 empty.textContent = __("No ratings yet.", "desktop-mode");
4062 wrap.appendChild(empty);
4063 return wrap;
4064 }
4065 for (let star = 5; star >= 1; star--) {
4066 const count = ratings[String(star)] ?? 0;
4067 const ratio = count / total;
4068 const row = document.createElement("div");
4069 row.className = "desktop-mode-plugins__histogram-row";
4070 const label = document.createElement("span");
4071 label.className = "desktop-mode-plugins__histogram-label";
4072 label.textContent = sprintf(
4073 /* translators: %d: number of stars (1–5) */
4074 __("%d �
4075 ", "desktop-mode"),
4076 star
4077 );
4078 const track = document.createElement("span");
4079 track.className = "desktop-mode-plugins__histogram-track";
4080 const fill = document.createElement("span");
4081 fill.className = "desktop-mode-plugins__histogram-fill";
4082 fill.style.width = `${Math.round(ratio * 100)}%`;
4083 track.appendChild(fill);
4084 const num = document.createElement("span");
4085 num.className = "desktop-mode-plugins__histogram-count";
4086 num.textContent = new Intl.NumberFormat().format(count);
4087 row.append(label, track, num);
4088 wrap.appendChild(row);
4089 }
4090 return wrap;
4091 }
4092 function buildReviewCard$1(item) {
4093 const card = document.createElement("article");
4094 card.className = "desktop-mode-plugins__review";
4095 const head = document.createElement("header");
4096 head.className = "desktop-mode-plugins__review-head";
4097 const author = document.createElement("span");
4098 author.className = "desktop-mode-plugins__review-author";
4099 author.textContent = item.author || __("Anonymous", "desktop-mode");
4100 const star = buildStarCluster(item.stars / 5 * 100, 0);
4101 head.append(author, star);
4102 if (item.date) {
4103 const date = document.createElement("time");
4104 date.className = "desktop-mode-plugins__review-date";
4105 date.textContent = item.date;
4106 head.appendChild(date);
4107 }
4108 const body = document.createElement("p");
4109 body.className = "desktop-mode-plugins__review-excerpt";
4110 body.textContent = item.excerpt;
4111 card.append(head, body);
4112 if (item.url) {
4113 const link = document.createElement("a");
4114 link.href = item.url;
4115 link.target = "_blank";
4116 link.rel = "noopener nofollow";
4117 link.textContent = __("Read on WordPress.org ↗", "desktop-mode");
4118 link.className = "desktop-mode-plugins__review-link";
4119 card.appendChild(link);
4120 }
4121 return card;
4122 }
4123 function buildSkeletonLines(count) {
4124 const wrap = document.createElement("div");
4125 wrap.className = "desktop-mode-plugins__skeleton";
4126 for (let i = 0; i < count; i++) {
4127 const line = document.createElement("span");
4128 line.className = "desktop-mode-plugins__skeleton-line";
4129 line.style.width = `${60 + i * 10 % 40}%`;
4130 wrap.appendChild(line);
4131 }
4132 return wrap;
4133 }
4134 function paintFooter(footer, slug, info, callbacks, close) {
4135 footer.replaceChildren();
4136 const cfg = getConfig();
4137 const installed = callbacks.getInstalled(slug);
4138 const left = document.createElement("div");
4139 left.className = "desktop-mode-plugins__flyout-footer-left";
4140 const wpOrg = document.createElement("a");
4141 wpOrg.href = `https://wordpress.org/plugins/${encodeURIComponent(slug)}/`;
4142 wpOrg.target = "_blank";
4143 wpOrg.rel = "noopener";
4144 wpOrg.className = "desktop-mode-plugins__flyout-wporg";
4145 wpOrg.textContent = __("View on WordPress.org ↗", "desktop-mode");
4146 left.appendChild(wpOrg);
4147 const right = document.createElement("div");
4148 right.className = "desktop-mode-plugins__flyout-footer-right";
4149 if (installed) {
4150 if (cfg.caps.activate) {
4151 if (installed.status === "active" || installed.status === "active-network") {
4152 const btn = button(__("Deactivate", "desktop-mode"), "secondary");
4153 btn.addEventListener("click", () => {
4154 void doDeactivate();
4155 });
4156 right.appendChild(btn);
4157 } else {
4158 const btn = button(__("Activate", "desktop-mode"), "primary");
4159 btn.addEventListener("click", () => {
4160 void doActivate();
4161 });
4162 right.appendChild(btn);
4163 }
4164 }
4165 if (cfg.caps.delete && installed.status === "inactive") {
4166 const btn = button(__("Delete", "desktop-mode"), "danger");
4167 btn.addEventListener("click", () => {
4168 void doDelete();
4169 });
4170 right.appendChild(btn);
4171 }
4172 } else if (cfg.caps.install) {
4173 const btn = button(__("Install", "desktop-mode"), "primary");
4174 btn.addEventListener("click", () => {
4175 void doInstall(btn);
4176 });
4177 right.appendChild(btn);
4178 }
4179 footer.append(left, right);
4180 async function doInstall(btn) {
4181 const originalText = btn.textContent ?? "";
4182 btn.setAttribute("busy", "");
4183 btn.setAttribute("disabled", "");
4184 btn.textContent = __("Installing…", "desktop-mode");
4185 try {
4186 const result = await installPluginBySlug(slug);
4187 toast$3(
4188 sprintf(
4189 /* translators: %s: plugin name */
4190 __("Installed %s.", "desktop-mode"),
4191 info?.name ?? slug
4192 )
4193 );
4194 await callbacks.onPluginInstalled(result.plugin ?? "", slug);
4195 paintFooter(footer, slug, info, callbacks, close);
4196 void refreshFrameworkMenu();
4197 } catch (err) {
4198 btn.removeAttribute("busy");
4199 btn.removeAttribute("disabled");
4200 btn.textContent = originalText;
4201 toast$3(
4202 sprintf(
4203 /* translators: %s: error message */
4204 __("Install failed: %s", "desktop-mode"),
4205 describe$2(err)
4206 ),
4207 6e3
4208 );
4209 }
4210 }
4211 async function doActivate() {
4212 if (!installed) {
4213 return;
4214 }
4215 try {
4216 const updated = await activateInstalledPlugin(installed);
4217 callbacks.onPluginActivated(updated);
4218 toast$3(
4219 sprintf(
4220 /* translators: %s: plugin name */
4221 __("%s activated.", "desktop-mode"),
4222 updated.name || updated.plugin
4223 )
4224 );
4225 paintFooter(footer, slug, info, callbacks, close);
4226 void refreshFrameworkMenu();
4227 } catch (err) {
4228 toast$3(
4229 sprintf(
4230 /* translators: %s: error message */
4231 __("Activation failed: %s", "desktop-mode"),
4232 describe$2(err)
4233 ),
4234 6e3
4235 );
4236 }
4237 }
4238 async function doDeactivate() {
4239 if (!installed) {
4240 return;
4241 }
4242 try {
4243 const updated = await deactivateInstalledPlugin(installed);
4244 callbacks.onPluginDeactivated(updated);
4245 if (isDesktopModeSelf(updated.plugin)) {
4246 toast$3(
4247 __(
4248 "Desktop Mode deactivated. Reloading…",
4249 "desktop-mode"
4250 ),
4251 2e3
4252 );
4253 reloadOutOfDesktopMode();
4254 return;
4255 }
4256 toast$3(
4257 sprintf(
4258 /* translators: %s: plugin name */
4259 __("%s deactivated.", "desktop-mode"),
4260 updated.name || updated.plugin
4261 )
4262 );
4263 paintFooter(footer, slug, info, callbacks, close);
4264 void refreshFrameworkMenu();
4265 } catch (err) {
4266 toast$3(
4267 sprintf(
4268 /* translators: %s: error message */
4269 __("Deactivation failed: %s", "desktop-mode"),
4270 describe$2(err)
4271 ),
4272 6e3
4273 );
4274 }
4275 }
4276 async function doDelete() {
4277 if (!installed) {
4278 return;
4279 }
4280 const ok = await confirm$1({
4281 title: __("Delete plugin?", "desktop-mode"),
4282 message: sprintf(
4283 /* translators: %s: plugin name */
4284 __(
4285 "Permanently delete %s? Its files will be removed from disk. This cannot be undone.",
4286 "desktop-mode"
4287 ),
4288 installed.name || installed.plugin
4289 ),
4290 confirmLabel: __("Delete", "desktop-mode"),
4291 danger: true
4292 });
4293 if (!ok) {
4294 return;
4295 }
4296 try {
4297 await deleteInstalledPlugin(installed);
4298 callbacks.onPluginDeleted(installed);
4299 if (isDesktopModeSelf(installed.plugin)) {
4300 toast$3(
4301 __(
4302 "Desktop Mode deleted. Reloading…",
4303 "desktop-mode"
4304 ),
4305 2e3
4306 );
4307 reloadOutOfDesktopMode();
4308 return;
4309 }
4310 toast$3(
4311 sprintf(
4312 /* translators: %s: plugin name */
4313 __("%s deleted.", "desktop-mode"),
4314 installed.name || installed.plugin
4315 )
4316 );
4317 close();
4318 void refreshFrameworkMenu();
4319 } catch (err) {
4320 toast$3(
4321 sprintf(
4322 /* translators: %s: error message */
4323 __("Delete failed: %s", "desktop-mode"),
4324 describe$2(err)
4325 ),
4326 6e3
4327 );
4328 }
4329 }
4330 }
4331 function button(label, variant) {
4332 const b = document.createElement("wpd-button");
4333 b.setAttribute("variant", variant);
4334 b.textContent = label;
4335 return b;
4336 }
4337 function isSafeUrl(raw) {
4338 const cleaned = Array.from(raw).filter((ch) => ch.charCodeAt(0) > 32).join("").toLowerCase();
4339 const scheme = cleaned.match(/^([a-z][a-z0-9+.-]*):/);
4340 if (!scheme) {
4341 return true;
4342 }
4343 return ["http", "https", "mailto", "tel"].includes(scheme[1]);
4344 }
4345 function sanitizeHtml$1(html2) {
4346 const allowed = /* @__PURE__ */ new Set([
4347 "A",
4348 "ABBR",
4349 "B",
4350 "BLOCKQUOTE",
4351 "BR",
4352 "CODE",
4353 "DD",
4354 "DEL",
4355 "DIV",
4356 "DL",
4357 "DT",
4358 "EM",
4359 "FIGCAPTION",
4360 "FIGURE",
4361 "H1",
4362 "H2",
4363 "H3",
4364 "H4",
4365 "H5",
4366 "H6",
4367 "HR",
4368 "I",
4369 "IMG",
4370 "KBD",
4371 "LI",
4372 "OL",
4373 "P",
4374 "PRE",
4375 "Q",
4376 "S",
4377 "SMALL",
4378 "SPAN",
4379 "STRONG",
4380 "SUB",
4381 "SUP",
4382 "TABLE",
4383 "TBODY",
4384 "TD",
4385 "TFOOT",
4386 "TH",
4387 "THEAD",
4388 "TR",
4389 "U",
4390 "UL"
4391 ]);
4392 const allowedAttrs = /* @__PURE__ */ new Set([
4393 "href",
4394 "src",
4395 "alt",
4396 "title",
4397 "name",
4398 "rel",
4399 "target",
4400 "colspan",
4401 "rowspan"
4402 ]);
4403 const wrap = document.createElement("div");
4404 wrap.innerHTML = html2;
4405 const walker = document.createTreeWalker(wrap, NodeFilter.SHOW_ELEMENT);
4406 const toRemove = [];
4407 let current = walker.currentNode;
4408 while (current) {
4409 const next = walker.nextNode();
4410 if (current === wrap) {
4411 current = next;
4412 continue;
4413 }
4414 if (!allowed.has(current.tagName)) {
4415 toRemove.push(current);
4416 } else {
4417 for (const attr of Array.from(current.attributes)) {
4418 if (!allowedAttrs.has(attr.name.toLowerCase())) {
4419 current.removeAttribute(attr.name);
4420 }
4421 }
4422 if (current.tagName === "A") {
4423 const href = current.getAttribute("href") ?? "";
4424 if (href && !isSafeUrl(href)) {
4425 current.removeAttribute("href");
4426 }
4427 }
4428 if (current.tagName === "IMG") {
4429 const src = current.getAttribute("src") ?? "";
4430 if (src && !isSafeUrl(src)) {
4431 current.removeAttribute("src");
4432 }
4433 }
4434 }
4435 current = next;
4436 }
4437 for (const el of toRemove) {
4438 const text = document.createTextNode(el.textContent ?? "");
4439 el.replaceWith(text);
4440 }
4441 return wrap.innerHTML;
4442 }
4443 function describe$2(err) {
4444 if (err instanceof Error) {
4445 return err.message;
4446 }
4447 return String(err);
4448 }
4449 function stripHtml$2(html2) {
4450 const tmp = document.createElement("div");
4451 tmp.innerHTML = html2;
4452 return tmp.textContent ?? "";
4453 }
4454 function humanDate$1(raw) {
4455 if (!raw) {
4456 return "";
4457 }
4458 const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raw);
4459 if (m) {
4460 const date = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
4461 try {
4462 return date.toLocaleDateString();
4463 } catch {
4464 return raw;
4465 }
4466 }
4467 return raw;
4468 }
4469 const CANARY_TAG = "wpd-confirm-dialog";
4470 let inflight = null;
4471 function isLoaded() {
4472 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
4473 }
4474 function injectScript(scriptUrl) {
4475 return new Promise((resolve, reject) => {
4476 const existing = document.querySelector(
4477 'script[data-desktop-mode-shell-overlays="1"]'
4478 );
4479 const finish = () => {
4480 if (isLoaded()) {
4481 resolve();
4482 return;
4483 }
4484 reject(
4485 new Error(
4486 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
4487 )
4488 );
4489 };
4490 if (existing) {
4491 if (isLoaded()) {
4492 finish();
4493 } else {
4494 existing.addEventListener("load", finish);
4495 existing.addEventListener(
4496 "error",
4497 () => reject(new Error("failed to load shell-overlays bundle"))
4498 );
4499 }
4500 return;
4501 }
4502 const s = document.createElement("script");
4503 s.src = scriptUrl;
4504 s.async = true;
4505 s.dataset.desktopModeShellOverlays = "1";
4506 s.addEventListener("load", finish);
4507 s.addEventListener(
4508 "error",
4509 () => reject(new Error("failed to load shell-overlays bundle"))
4510 );
4511 document.head.appendChild(s);
4512 });
4513 }
4514 function ensureShellOverlaysLoaded(scriptUrl) {
4515 if (isLoaded()) {
4516 return Promise.resolve();
4517 }
4518 if (!scriptUrl) {
4519 return Promise.resolve();
4520 }
4521 if (!inflight) {
4522 inflight = injectScript(scriptUrl);
4523 }
4524 return inflight;
4525 }
4526 function shellOverlaysBundleUrl() {
4527 const cfg = window.desktopModeConfig;
4528 return cfg?.shellOverlaysBundleUrl ?? "";
4529 }
4530 function openWithShellOverlays(isStillCurrent, fn) {
4531 const url = shellOverlaysBundleUrl();
4532 if (isLoaded() || !url) {
4533 fn();
4534 return;
4535 }
4536 void ensureShellOverlaysLoaded(url).then(() => {
4537 if (!isStillCurrent()) {
4538 return;
4539 }
4540 fn();
4541 }).catch((err) => {
4542 if (typeof console !== "undefined") {
4543 console.warn(
4544 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
4545 err
4546 );
4547 }
4548 });
4549 }
4550 async function wpdConfirm(options) {
4551 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
4552 return new Promise((resolve) => {
4553 const dialog = document.createElement("wpd-confirm-dialog");
4554 dialog.setAttribute("open", "");
4555 if (options.title) {
4556 dialog.setAttribute("title", options.title);
4557 }
4558 dialog.setAttribute("message", options.message);
4559 if (options.confirmLabel) {
4560 dialog.setAttribute("confirm-label", options.confirmLabel);
4561 }
4562 if (options.cancelLabel) {
4563 dialog.setAttribute("cancel-label", options.cancelLabel);
4564 }
4565 {
4566 dialog.setAttribute("danger", "");
4567 }
4568 if (options.hideCancel) {
4569 dialog.setAttribute("hide-cancel", "");
4570 }
4571 if (options.dismissable) {
4572 dialog.setAttribute("dismissable", "");
4573 }
4574 const cleanup = (ok) => {
4575 dialog.remove();
4576 resolve(ok);
4577 };
4578 dialog.addEventListener("wpd-confirm", () => cleanup(true));
4579 dialog.addEventListener("wpd-cancel", () => cleanup(false));
4580 document.body.appendChild(dialog);
4581 const inner = dialog.shadowRoot?.querySelector(".dialog");
4582 (inner ?? dialog).focus?.();
4583 });
4584 }
4585 const DEFAULT_DURATION_MS = 4e3;
4586 const FADE_OUT_MS = 200;
4587 function showToast(options) {
4588 const intent = activity.filter(
4589 "desktop-mode/toast-requested",
4590 { ...options }
4591 );
4592 if (!intent || intent.cancel === true) {
4593 return () => void 0;
4594 }
4595 let dismissRequested = false;
4596 let realDismiss = null;
4597 openWithShellOverlays(
4598 () => !dismissRequested,
4599 () => {
4600 realDismiss = renderToast(intent);
4601 }
4602 );
4603 return () => {
4604 dismissRequested = true;
4605 if (realDismiss) {
4606 realDismiss();
4607 }
4608 };
4609 }
4610 function renderToast(intent) {
4611 const container = ensureContainer();
4612 const toast2 = document.createElement("wpd-toast");
4613 toast2.textContent = intent.message;
4614 if (intent.action) {
4615 toast2.setAttribute("action", intent.action.label);
4616 toast2.addEventListener("wpd-toast-action", () => {
4617 intent.action?.onClick();
4618 dismiss();
4619 });
4620 }
4621 container.appendChild(toast2);
4622 let dismissed = false;
4623 let dismissTimer = null;
4624 const dismiss = () => {
4625 if (dismissed) {
4626 return;
4627 }
4628 dismissed = true;
4629 if (dismissTimer !== null) {
4630 window.clearTimeout(dismissTimer);
4631 dismissTimer = null;
4632 }
4633 toast2.setAttribute("state", "out");
4634 window.setTimeout(() => {
4635 toast2.remove();
4636 }, FADE_OUT_MS);
4637 };
4638 requestAnimationFrame(() => {
4639 toast2.setAttribute("state", "in");
4640 });
4641 dismissTimer = window.setTimeout(
4642 dismiss,
4643 intent.duration ?? DEFAULT_DURATION_MS
4644 );
4645 activity.publish("desktop-mode/toast-shown", { ...intent });
4646 return dismiss;
4647 }
4648 function ensureContainer() {
4649 const existing = document.querySelector(
4650 "wpd-toast-container"
4651 );
4652 if (existing) {
4653 return existing;
4654 }
4655 const el = document.createElement("wpd-toast-container");
4656 document.body.appendChild(el);
4657 return el;
4658 }
4659 const PLUGINS_CHANGED_TOPIC$3 = "desktop-mode.plugin.changed";
4660 const PLUGINS_CHANGED_SOURCE = "upload-dialog";
4661 function openUploadDialog(host, prefilled, callbacks = {}) {
4662 return new Promise((resolve) => {
4663 const overlay = document.createElement("div");
4664 overlay.className = "desktop-mode-plugins__upload-overlay";
4665 const card = document.createElement("div");
4666 card.className = "desktop-mode-plugins__upload-card";
4667 card.setAttribute("role", "dialog");
4668 card.setAttribute("aria-modal", "true");
4669 card.setAttribute(
4670 "aria-label",
4671 __("Upload a plugin .zip", "desktop-mode")
4672 );
4673 const heading = document.createElement("h2");
4674 heading.className = "desktop-mode-plugins__upload-heading";
4675 heading.textContent = __("Upload a plugin", "desktop-mode");
4676 const lede = document.createElement("p");
4677 lede.className = "desktop-mode-plugins__upload-lede";
4678 lede.textContent = __(
4679 "Pick a .zip file from your computer, or drop one onto the area below.",
4680 "desktop-mode"
4681 );
4682 const dropZone = document.createElement("div");
4683 dropZone.className = "desktop-mode-plugins__upload-dropzone";
4684 dropZone.tabIndex = 0;
4685 dropZone.setAttribute("role", "button");
4686 dropZone.setAttribute(
4687 "aria-label",
4688 __(
4689 "Drop a .zip plugin file here, or click to choose a file.",
4690 "desktop-mode"
4691 )
4692 );
4693 const dropIcon = document.createElement("span");
4694 dropIcon.className = "dashicons dashicons-upload desktop-mode-plugins__upload-icon";
4695 dropIcon.setAttribute("aria-hidden", "true");
4696 const dropHint = document.createElement("p");
4697 dropHint.className = "desktop-mode-plugins__upload-hint";
4698 dropHint.textContent = __(
4699 "Drop your .zip here or click to browse",
4700 "desktop-mode"
4701 );
4702 const fileLabel = document.createElement("p");
4703 fileLabel.className = "desktop-mode-plugins__upload-filename";
4704 fileLabel.hidden = true;
4705 dropZone.append(dropIcon, dropHint, fileLabel);
4706 const input = document.createElement("input");
4707 input.type = "file";
4708 input.accept = ".zip,application/zip,application/x-zip-compressed";
4709 input.style.display = "none";
4710 dropZone.appendChild(input);
4711 const status = document.createElement("p");
4712 status.className = "desktop-mode-plugins__upload-status";
4713 status.hidden = true;
4714 const actions = document.createElement("div");
4715 actions.className = "desktop-mode-plugins__upload-actions";
4716 const cancelBtn = document.createElement("wpd-button");
4717 cancelBtn.setAttribute("variant", "ghost");
4718 cancelBtn.textContent = __("Cancel", "desktop-mode");
4719 const submitBtn = document.createElement("wpd-button");
4720 submitBtn.setAttribute("variant", "primary");
4721 submitBtn.textContent = __("Install", "desktop-mode");
4722 submitBtn.setAttribute("disabled", "");
4723 actions.append(cancelBtn, submitBtn);
4724 card.append(heading, lede, dropZone, status, actions);
4725 overlay.appendChild(card);
4726 host.appendChild(overlay);
4727 const swallowDrag = (ev) => {
4728 ev.preventDefault();
4729 ev.stopPropagation();
4730 };
4731 overlay.addEventListener("dragenter", swallowDrag);
4732 overlay.addEventListener("dragover", swallowDrag);
4733 overlay.addEventListener("drop", swallowDrag);
4734 let pickedFile = null;
4735 let uploading = false;
4736 const setFile = (file) => {
4737 pickedFile = file;
4738 if (file) {
4739 dropZone.classList.add("has-file");
4740 fileLabel.hidden = false;
4741 fileLabel.textContent = sprintf(
4742 /* translators: 1: file name, 2: file size in KB */
4743 __("%1$s · %2$s KB", "desktop-mode"),
4744 file.name,
4745 Math.round(file.size / 1024).toString()
4746 );
4747 submitBtn.removeAttribute("disabled");
4748 } else {
4749 dropZone.classList.remove("has-file");
4750 fileLabel.hidden = true;
4751 submitBtn.setAttribute("disabled", "");
4752 }
4753 };
4754 dropZone.addEventListener("click", (ev) => {
4755 if (ev.target?.tagName === "INPUT") {
4756 return;
4757 }
4758 input.click();
4759 });
4760 dropZone.addEventListener("keydown", (ev) => {
4761 if (ev.key === "Enter" || ev.key === " ") {
4762 ev.preventDefault();
4763 input.click();
4764 }
4765 });
4766 dropZone.addEventListener("dragover", (ev) => {
4767 ev.preventDefault();
4768 ev.stopPropagation();
4769 dropZone.classList.add("is-hovered");
4770 });
4771 dropZone.addEventListener("dragleave", (ev) => {
4772 ev.stopPropagation();
4773 dropZone.classList.remove("is-hovered");
4774 });
4775 dropZone.addEventListener("drop", (ev) => {
4776 ev.preventDefault();
4777 ev.stopPropagation();
4778 dropZone.classList.remove("is-hovered");
4779 const file = ev.dataTransfer?.files?.[0];
4780 if (file && isZip(file)) {
4781 setFile(file);
4782 } else if (file) {
4783 showStatus(
4784 __("Only .zip files are accepted.", "desktop-mode"),
4785 "error"
4786 );
4787 }
4788 });
4789 input.addEventListener("change", () => {
4790 const file = input.files?.[0];
4791 if (file && isZip(file)) {
4792 setFile(file);
4793 }
4794 });
4795 const close = (result) => {
4796 document.removeEventListener("keydown", onKey);
4797 overlay.remove();
4798 resolve(result);
4799 };
4800 const onKey = (ev) => {
4801 if (ev.key === "Escape" && !uploading) {
4802 close(null);
4803 }
4804 };
4805 document.addEventListener("keydown", onKey);
4806 cancelBtn.addEventListener("click", () => {
4807 if (uploading) {
4808 return;
4809 }
4810 close(null);
4811 });
4812 submitBtn.addEventListener("click", () => {
4813 if (!pickedFile || uploading) {
4814 return;
4815 }
4816 void runUpload();
4817 });
4818 overlay.addEventListener("click", (ev) => {
4819 if (ev.target === overlay && !uploading) {
4820 close(null);
4821 }
4822 });
4823 if (prefilled && isZip(prefilled)) {
4824 setFile(prefilled);
4825 }
4826 async function runUpload(overwrite = false) {
4827 if (!pickedFile) {
4828 return;
4829 }
4830 uploading = true;
4831 submitBtn.setAttribute("busy", "");
4832 submitBtn.setAttribute("disabled", "");
4833 cancelBtn.setAttribute("disabled", "");
4834 showStatus(
4835 overwrite ? __("Replacing existing plugin…", "desktop-mode") : __("Uploading and installing…", "desktop-mode"),
4836 "info"
4837 );
4838 try {
4839 const result = await uploadPluginZip(pickedFile, { overwrite });
4840 if (callbacks.onUploaded) {
4841 callbacks.onUploaded(result);
4842 }
4843 broadcast(PLUGINS_CHANGED_TOPIC$3, {
4844 source: PLUGINS_CHANGED_SOURCE,
4845 plugin: result.plugin_file,
4846 action: "install"
4847 });
4848 void refreshFrameworkMenu();
4849 showSuccessPanel(result);
4850 } catch (err) {
4851 const errStatus = err.status;
4852 const errCode = err.code;
4853 if (!overwrite && (errStatus === 409 || errCode === "folder_exists")) {
4854 uploading = false;
4855 submitBtn.removeAttribute("busy");
4856 submitBtn.removeAttribute("disabled");
4857 cancelBtn.removeAttribute("disabled");
4858 showStatus(
4859 __(
4860 "A plugin with the same folder name is already installed.",
4861 "desktop-mode"
4862 ),
4863 "info"
4864 );
4865 const ok = await wpdConfirm({
4866 title: __("Replace existing plugin?", "desktop-mode"),
4867 message: __(
4868 "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.",
4869 "desktop-mode"
4870 ),
4871 confirmLabel: __("Replace", "desktop-mode"),
4872 cancelLabel: __("Cancel", "desktop-mode")
4873 });
4874 if (ok) {
4875 await runUpload(true);
4876 }
4877 return;
4878 }
4879 uploading = false;
4880 submitBtn.removeAttribute("busy");
4881 submitBtn.removeAttribute("disabled");
4882 cancelBtn.removeAttribute("disabled");
4883 const message = err instanceof Error ? err.message : String(err);
4884 showStatus(
4885 sprintf(
4886 /* translators: %s: error message from the upload handler */
4887 __("Upload failed: %s", "desktop-mode"),
4888 message
4889 ),
4890 "error"
4891 );
4892 }
4893 }
4894 function showSuccessPanel(result) {
4895 uploading = false;
4896 dropZone.remove();
4897 input.remove();
4898 actions.remove();
4899 status.hidden = true;
4900 const successHeading = document.createElement("h3");
4901 successHeading.className = "desktop-mode-plugins__upload-success-heading";
4902 successHeading.textContent = __(
4903 "Plugin installed successfully.",
4904 "desktop-mode"
4905 );
4906 const detail = document.createElement("p");
4907 detail.className = "desktop-mode-plugins__upload-success-detail";
4908 const name = result.plugin_name || result.plugin_file;
4909 detail.textContent = result.plugin_version ? sprintf(
4910 /* translators: 1: plugin name 2: plugin version */
4911 __("%1$s %2$s", "desktop-mode"),
4912 name,
4913 result.plugin_version
4914 ) : name;
4915 const successActions = document.createElement("div");
4916 successActions.className = "desktop-mode-plugins__upload-actions";
4917 const closeBtn = document.createElement("wpd-button");
4918 closeBtn.setAttribute("variant", "ghost");
4919 closeBtn.textContent = __("Close", "desktop-mode");
4920 const activateBtn = document.createElement("wpd-button");
4921 activateBtn.setAttribute("variant", "primary");
4922 activateBtn.textContent = __("Activate Plugin", "desktop-mode");
4923 successActions.append(closeBtn, activateBtn);
4924 card.append(successHeading, detail, successActions);
4925 closeBtn.addEventListener("click", () => {
4926 if (uploading) {
4927 return;
4928 }
4929 close(result);
4930 });
4931 activateBtn.addEventListener("click", () => {
4932 if (uploading) {
4933 return;
4934 }
4935 void runActivate();
4936 });
4937 async function runActivate() {
4938 uploading = true;
4939 activateBtn.setAttribute("busy", "");
4940 activateBtn.setAttribute("disabled", "");
4941 closeBtn.setAttribute("disabled", "");
4942 try {
4943 const pluginFile = result.plugin_file.endsWith(".php") ? result.plugin_file.slice(0, -4) : result.plugin_file;
4944 const updated = await activateInstalledPlugin({
4945 plugin: pluginFile,
4946 status: "inactive"
4947 });
4948 if (callbacks.onActivated) {
4949 callbacks.onActivated(result.plugin_file);
4950 }
4951 void refreshFrameworkMenu();
4952 broadcast(PLUGINS_CHANGED_TOPIC$3, {
4953 source: PLUGINS_CHANGED_SOURCE,
4954 plugin: updated.plugin,
4955 action: "activate"
4956 });
4957 showToast({
4958 message: sprintf(
4959 /* translators: %s: plugin name */
4960 __("%s activated.", "desktop-mode"),
4961 name
4962 )
4963 });
4964 uploading = false;
4965 successHeading.textContent = __(
4966 "Plugin activated.",
4967 "desktop-mode"
4968 );
4969 activateBtn.remove();
4970 closeBtn.removeAttribute("disabled");
4971 closeBtn.setAttribute("variant", "primary");
4972 closeBtn.textContent = __("Done", "desktop-mode");
4973 closeBtn.focus?.();
4974 } catch (err) {
4975 uploading = false;
4976 activateBtn.removeAttribute("busy");
4977 activateBtn.removeAttribute("disabled");
4978 closeBtn.removeAttribute("disabled");
4979 const message = err instanceof Error ? err.message : String(err);
4980 status.hidden = false;
4981 status.dataset.tone = "error";
4982 status.textContent = sprintf(
4983 /* translators: %s: error message from the activate handler */
4984 __("Activate failed: %s", "desktop-mode"),
4985 message
4986 );
4987 card.appendChild(status);
4988 }
4989 }
4990 window.setTimeout(() => activateBtn.focus?.(), 16);
4991 }
4992 function showStatus(message, tone) {
4993 status.hidden = false;
4994 status.dataset.tone = tone;
4995 status.textContent = message;
4996 }
4997 window.setTimeout(() => dropZone.focus(), 16);
4998 });
4999 }
5000 function isZip(file) {
5001 if (file.size <= 0) {
5002 return false;
5003 }
5004 const name = file.name.toLowerCase();
5005 if (name.endsWith(".zip")) {
5006 return true;
5007 }
5008 return file.type === "application/zip" || file.type === "application/x-zip-compressed";
5009 }
5010 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}`;
5011 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}`;
5012 const _WpdSegment = class _WpdSegment extends Component {
5013 render() {
5014 this.setAttribute("role", "radio");
5015 return html`
5016 <button type="button" @click=${() => this._onPick()}>
5017 <slot></slot>
5018 </button>
5019 `;
5020 }
5021 _onPick() {
5022 this.emit("wpd-segment-pick", {
5023 value: this.value
5024 });
5025 }
5026 };
5027 _WpdSegment.props = ["value"];
5028 _WpdSegment.styles = [segmentStyles];
5029 _WpdSegment.help = {
5030 title: "Segment",
5031 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
5032 status: "stable",
5033 since: "0.9.0",
5034 props: [
5035 {
5036 name: "value",
5037 type: "string",
5038 description: "Identifier this segment contributes to the parent group selection."
5039 }
5040 ],
5041 slots: [
5042 { name: "(default)", description: "Visible segment label." }
5043 ],
5044 events: [
5045 {
5046 name: "wpd-segment-pick",
5047 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
5048 detail: "{ value: string }"
5049 }
5050 ]
5051 };
5052 let WpdSegment = _WpdSegment;
5053 defineComponent("wpd-segment", WpdSegment);
5054 const _WpdSegmented = class _WpdSegmented extends Component {
5055 connectedCallback() {
5056 super.connectedCallback();
5057 this.addEventListener("wpd-segment-pick", (e) => {
5058 const detail = e.detail;
5059 e.stopPropagation();
5060 this.value = detail.value;
5061 this.emit("wpd-pick", { value: detail.value });
5062 });
5063 }
5064 /**
5065 * Declarative item-list setter. Replaces the existing
5066 * `<wpd-segment>` children with a fresh set built from a
5067 * `{ value, label }` array; preserves the current selection
5068 * when the value still matches an entry, otherwise falls back
5069 * to the first item.
5070 *
5071 * Collapses the pre-0.11 imperative dance (clear children,
5072 * `createElement`, set `textContent`, `appendChild`, then
5073 * `setAttribute('value', …)` on the group — order matters) to
5074 * a single assignment:
5075 *
5076 * ```js
5077 * segmented.items = [
5078 * { value: 'm', label: 'm' },
5079 * { value: 'km', label: 'km' },
5080 * ];
5081 * ```
5082 *
5083 * @since 0.5.0
5084 */
5085 set items(list) {
5086 const existing = this.querySelectorAll(":scope > wpd-segment");
5087 for (const el of Array.from(existing)) {
5088 el.remove();
5089 }
5090 for (const item of list) {
5091 const seg = document.createElement("wpd-segment");
5092 seg.setAttribute("value", item.value);
5093 seg.textContent = item.label;
5094 this.appendChild(seg);
5095 }
5096 const current = this.value;
5097 const stillValid = current !== null && list.some((i) => i.value === current);
5098 if (!stillValid && list.length > 0) {
5099 this.value = list[0].value;
5100 } else {
5101 this.requestUpdate();
5102 }
5103 }
5104 render() {
5105 const label = this.label || "";
5106 if (label) {
5107 this.setAttribute("aria-label", label);
5108 }
5109 this.setAttribute("role", "radiogroup");
5110 const current = this.value;
5111 queueMicrotask(() => {
5112 const segs = this.querySelectorAll("wpd-segment");
5113 for (const seg of Array.from(segs)) {
5114 const v = seg.getAttribute("value");
5115 seg.setAttribute(
5116 "aria-checked",
5117 v === current ? "true" : "false"
5118 );
5119 }
5120 });
5121 return html`<slot></slot>`;
5122 }
5123 };
5124 _WpdSegmented.props = ["value", "label"];
5125 _WpdSegmented.styles = [segmentedStyles];
5126 _WpdSegmented.help = {
5127 title: "Segmented",
5128 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
5129 status: "stable",
5130 since: "0.9.0",
5131 props: [
5132 {
5133 name: "value",
5134 type: "string",
5135 description: "Currently selected segment value. Mirrored onto child aria-checked."
5136 },
5137 {
5138 name: "label",
5139 type: "string",
5140 description: "aria-label for the radiogroup."
5141 }
5142 ],
5143 slots: [
5144 { name: "(default)", description: '<wpd-segment value="…"> children.' }
5145 ],
5146 events: [
5147 {
5148 name: "wpd-pick",
5149 description: "Fires when the selected segment changes.",
5150 detail: "{ value: string }"
5151 }
5152 ],
5153 cssProps: [
5154 { name: "--desktop-mode-window-bg", description: "Pill background." },
5155 { name: "--desktop-mode-text", description: "Active label colour." },
5156 { name: "--desktop-mode-muted", description: "Inactive label colour." }
5157 ],
5158 example: html`
5159 <wpd-segmented value="md" label="Dock size">
5160 <wpd-segment value="sm">Small</wpd-segment>
5161 <wpd-segment value="md">Medium</wpd-segment>
5162 <wpd-segment value="lg">Large</wpd-segment>
5163 </wpd-segmented>
5164 `
5165 };
5166 let WpdSegmented = _WpdSegmented;
5167 defineComponent("wpd-segmented", WpdSegmented);
5168 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}`;
5169 const _WpdTextField = class _WpdTextField extends Component {
5170 constructor() {
5171 super(...arguments);
5172 this._revealed = false;
5173 }
5174 connectedCallback() {
5175 super.connectedCallback();
5176 ensureAutoId(this);
5177 }
5178 render() {
5179 const label = this.label || "";
5180 const value = this.value ?? "";
5181 const placeholder = this.placeholder || "";
5182 const disabled = this.disabled !== null;
5183 const readonly = this.readonly !== null;
5184 const declaredAutocomplete = this.autocomplete;
5185 const declaredType = this.type || "text";
5186 const isPassword = declaredType === "password";
5187 let autocomplete = declaredAutocomplete || "off";
5188 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
5189 autocomplete = "new-password";
5190 }
5191 const maxLength = this.maxlength;
5192 const minLength = this.minlength;
5193 const pattern = this.pattern || "";
5194 const name = this.name || "";
5195 const suffix = this.suffix || "";
5196 const invalid = this.invalid !== null;
5197 const reveal = this.reveal !== null;
5198 const isPasswordIntent = declaredType === "password";
5199 const isMasked = isPasswordIntent && !(reveal && this._revealed);
5200 let effectiveType;
5201 if (isPasswordIntent) {
5202 effectiveType = "text";
5203 } else if (reveal && this._revealed) {
5204 effectiveType = "text";
5205 } else {
5206 effectiveType = declaredType;
5207 }
5208 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
5209 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
5210 const hostId = this.id || "wpd-unnamed";
5211 const inputId = `${hostId}__input`;
5212 return html`
5213 ${label ? html`<label
5214 class="wpd-text-field__label"
5215 for=${inputId}
5216 >${label}</label>` : html``}
5217 <span class=${rowClass}>
5218 <input
5219 id=${inputId}
5220 class=${inputClass}
5221 type=${effectiveType}
5222 .value=${value}
5223 placeholder=${placeholder}
5224 ?disabled=${disabled}
5225 ?readonly=${readonly}
5226 autocomplete=${autocomplete}
5227 maxlength=${maxLength ?? ""}
5228 minlength=${minLength ?? ""}
5229 pattern=${pattern}
5230 name=${name}
5231 aria-invalid=${invalid ? "true" : "false"}
5232 aria-label=${label || ""}
5233 @input=${(e) => this._onInput(e)}
5234 @change=${(e) => this._onChange(e)}
5235 @keydown=${(e) => this._onKeyDown(e)}
5236 />
5237 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
5238 ${reveal ? this._renderRevealButton(disabled) : html``}
5239 </span>
5240 `;
5241 }
5242 _renderRevealButton(disabled) {
5243 const label = this._revealed ? "Hide" : "Show";
5244 return html`
5245 <button
5246 type="button"
5247 class="wpd-text-field__reveal"
5248 aria-label=${label}
5249 aria-pressed=${this._revealed ? "true" : "false"}
5250 ?disabled=${disabled}
5251 tabindex="0"
5252 @click=${() => this._onToggleReveal()}
5253 >
5254 ${this._revealed ? _iconEyeOff() : _iconEye()}
5255 </button>
5256 `;
5257 }
5258 _onToggleReveal() {
5259 this._revealed = !this._revealed;
5260 this.requestUpdate();
5261 }
5262 _onInput(e) {
5263 const input = e.target;
5264 this.value = input.value;
5265 this.emit("wpd-input-change", { value: input.value });
5266 }
5267 _onChange(e) {
5268 const input = e.target;
5269 this.emit("wpd-input-commit", { value: input.value });
5270 }
5271 _onKeyDown(e) {
5272 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
5273 const input = e.target;
5274 this.emit("wpd-submit", { value: input.value });
5275 }
5276 }
5277 };
5278 _WpdTextField.props = [
5279 "label",
5280 "value",
5281 "placeholder",
5282 "disabled",
5283 "readonly",
5284 "autocomplete",
5285 "type",
5286 "maxlength",
5287 "minlength",
5288 "pattern",
5289 "name",
5290 "suffix",
5291 "invalid",
5292 "reveal"
5293 ];
5294 _WpdTextField.styles = [textFieldStyles];
5295 _WpdTextField.help = {
5296 title: "Text field",
5297 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.",
5298 status: "stable",
5299 since: "0.5.0",
5300 props: [
5301 { name: "label", type: "string", description: "Visible label above the input." },
5302 { name: "value", type: "string", description: "Current input value; reflected two-way." },
5303 { name: "placeholder", type: "string", description: "Native placeholder string." },
5304 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
5305 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
5306 {
5307 name: "autocomplete",
5308 type: "string",
5309 default: "off",
5310 description: "Forwarded to the native input autocomplete attribute."
5311 },
5312 {
5313 name: "type",
5314 type: "string",
5315 default: "text",
5316 description: "Native input type (text, password, email, search, tel, url)."
5317 },
5318 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
5319 { name: "minlength", type: "integer (string)", description: "Native minlength." },
5320 { name: "pattern", type: "regex string", description: "Native validation pattern." },
5321 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
5322 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
5323 {
5324 name: "invalid",
5325 type: "boolean attribute",
5326 description: "Marks the field aria-invalid and applies the error style."
5327 },
5328 {
5329 name: "reveal",
5330 type: "boolean attribute",
5331 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
5332 }
5333 ],
5334 events: [
5335 {
5336 name: "wpd-input-change",
5337 description: "Fires on every input keystroke.",
5338 detail: "{ value: string }"
5339 },
5340 {
5341 name: "wpd-input-commit",
5342 description: "Fires on the native change event (blur / Enter).",
5343 detail: "{ value: string }"
5344 },
5345 {
5346 name: "wpd-submit",
5347 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
5348 detail: "{ value: string }"
5349 }
5350 ],
5351 cssProps: [
5352 { name: "--desktop-mode-text", description: "Text colour." },
5353 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
5354 { name: "--desktop-mode-border", description: "Input outline." },
5355 { name: "--desktop-mode-window-bg", description: "Input background." }
5356 ],
5357 example: html`
5358 <wpd-stack gap="8">
5359 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
5360 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
5361 </wpd-stack>
5362 `
5363 };
5364 let WpdTextField = _WpdTextField;
5365 defineComponent("wpd-text-field", WpdTextField);
5366 function _iconEye() {
5367 return html`
5368 <svg
5369 viewBox="0 0 16 16"
5370 width="14"
5371 height="14"
5372 fill="none"
5373 stroke="currentColor"
5374 stroke-width="1.5"
5375 stroke-linecap="round"
5376 stroke-linejoin="round"
5377 aria-hidden="true"
5378 focusable="false"
5379 >
5380 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
5381 <circle cx="8" cy="8" r="2" />
5382 </svg>
5383 `;
5384 }
5385 function _iconEyeOff() {
5386 return html`
5387 <svg
5388 viewBox="0 0 16 16"
5389 width="14"
5390 height="14"
5391 fill="none"
5392 stroke="currentColor"
5393 stroke-width="1.5"
5394 stroke-linecap="round"
5395 stroke-linejoin="round"
5396 aria-hidden="true"
5397 focusable="false"
5398 >
5399 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
5400 <circle cx="8" cy="8" r="2" />
5401 <line x1="2" y1="2" x2="14" y2="14" />
5402 </svg>
5403 `;
5404 }
5405 const PLUGINS_CHANGED_TOPIC$2 = "desktop-mode.plugin.changed";
5406 const SOURCE$2 = "browse-view";
5407 function toast$2(message, duration = 3500) {
5408 const api2 = window.wp?.desktop;
5409 if (api2 && typeof api2.showToast === "function") {
5410 api2.showToast({ message, duration });
5411 return;
5412 }
5413 console.log("[plugins-window]", message);
5414 }
5415 function mountBrowseView(host, flyoutEl, bodyEl) {
5416 host.replaceChildren();
5417 const state = {
5418 filter: "featured",
5419 search: "",
5420 page: 1,
5421 totalPages: 0,
5422 loading: false,
5423 exhausted: false,
5424 plugins: [],
5425 installed: /* @__PURE__ */ new Map(),
5426 cardsBySlug: /* @__PURE__ */ new Map()
5427 };
5428 const toolbar = document.createElement("header");
5429 toolbar.className = "desktop-mode-plugins__toolbar";
5430 const left = document.createElement("div");
5431 left.className = "desktop-mode-plugins__toolbar-left";
5432 const segmented = document.createElement("wpd-segmented");
5433 segmented.setAttribute("value", "featured");
5434 const filters = [
5435 { value: "featured", label: __("Featured", "desktop-mode") },
5436 { value: "popular", label: __("Popular", "desktop-mode") },
5437 { value: "recommended", label: __("Recommended", "desktop-mode") },
5438 { value: "favorites", label: __("Favorites", "desktop-mode") },
5439 { value: "new", label: __("New", "desktop-mode") },
5440 { value: "beta", label: __("Beta", "desktop-mode") }
5441 ];
5442 for (const opt of filters) {
5443 const seg = document.createElement("wpd-segment");
5444 seg.setAttribute("value", opt.value);
5445 seg.textContent = opt.label;
5446 segmented.appendChild(seg);
5447 }
5448 segmented.addEventListener("wpd-pick", (ev) => {
5449 const next = ev.detail?.value ?? "featured";
5450 state.filter = next;
5451 void resetAndLoad();
5452 });
5453 const search = document.createElement("wpd-text-field");
5454 search.setAttribute("placeholder", __("Search WordPress.org…", "desktop-mode"));
5455 let searchDebounce;
5456 search.addEventListener("wpd-input-change", (ev) => {
5457 const value = ev.detail?.value ?? "";
5458 window.clearTimeout(searchDebounce);
5459 searchDebounce = window.setTimeout(() => {
5460 state.search = value;
5461 void resetAndLoad();
5462 }, 250);
5463 });
5464 left.append(segmented, search);
5465 const right = document.createElement("div");
5466 right.className = "desktop-mode-plugins__toolbar-trailing";
5467 const cfg = getConfig();
5468 if (cfg.caps.upload) {
5469 const upload = document.createElement("wpd-button");
5470 upload.setAttribute("variant", "secondary");
5471 upload.innerHTML = '<span class="dashicons dashicons-upload" aria-hidden="true"></span> ' + __("Upload Plugin", "desktop-mode");
5472 upload.addEventListener("click", () => {
5473 void openUploadDialog(bodyEl, null, {
5474 onUploaded: () => void refreshInstalled()
5475 });
5476 });
5477 right.appendChild(upload);
5478 }
5479 const refreshButton = document.createElement("wpd-button");
5480 refreshButton.setAttribute("variant", "ghost");
5481 refreshButton.setAttribute("title", __("Refresh", "desktop-mode"));
5482 refreshButton.innerHTML = '<span class="dashicons dashicons-update" aria-hidden="true"></span>';
5483 refreshButton.addEventListener("click", () => {
5484 void refreshInstalled();
5485 void resetAndLoad();
5486 });
5487 right.appendChild(refreshButton);
5488 toolbar.append(left, right);
5489 const gallery = document.createElement("div");
5490 gallery.className = "desktop-mode-plugins__gallery";
5491 const sentinel = document.createElement("div");
5492 sentinel.className = "desktop-mode-plugins__gallery-sentinel";
5493 sentinel.setAttribute("aria-hidden", "true");
5494 const status = document.createElement("p");
5495 status.className = "desktop-mode-plugins__gallery-status";
5496 status.hidden = true;
5497 host.append(toolbar, gallery, status);
5498 const dropOverlay = document.createElement("div");
5499 dropOverlay.className = "desktop-mode-plugins__window-drop";
5500 dropOverlay.setAttribute("aria-hidden", "true");
5501 const dropMsg = document.createElement("p");
5502 dropMsg.textContent = __(
5503 "Drop the .zip to install.",
5504 "desktop-mode"
5505 );
5506 dropOverlay.appendChild(dropMsg);
5507 bodyEl.appendChild(dropOverlay);
5508 let dragDepth = 0;
5509 const isZipDrag = (ev) => Boolean(
5510 ev.dataTransfer?.types.includes("Files")
5511 );
5512 const onDragEnter = (ev) => {
5513 if (!cfg.caps.upload) {
5514 return;
5515 }
5516 if (!isZipDrag(ev)) {
5517 return;
5518 }
5519 dragDepth++;
5520 bodyEl.classList.add("has-zip-dragover");
5521 };
5522 const onDragLeave = (ev) => {
5523 if (!cfg.caps.upload || !isZipDrag(ev)) {
5524 return;
5525 }
5526 dragDepth = Math.max(0, dragDepth - 1);
5527 if (dragDepth === 0) {
5528 bodyEl.classList.remove("has-zip-dragover");
5529 }
5530 };
5531 const onDragOver = (ev) => {
5532 if (cfg.caps.upload && isZipDrag(ev)) {
5533 ev.preventDefault();
5534 }
5535 };
5536 const onDrop = (ev) => {
5537 if (!cfg.caps.upload) {
5538 return;
5539 }
5540 const file = ev.dataTransfer?.files?.[0];
5541 dragDepth = 0;
5542 bodyEl.classList.remove("has-zip-dragover");
5543 if (!file) {
5544 return;
5545 }
5546 ev.preventDefault();
5547 void openUploadDialog(bodyEl, file, {
5548 onUploaded: () => void refreshInstalled()
5549 });
5550 };
5551 bodyEl.addEventListener("dragenter", onDragEnter);
5552 bodyEl.addEventListener("dragleave", onDragLeave);
5553 bodyEl.addEventListener("dragover", onDragOver);
5554 bodyEl.addEventListener("drop", onDrop);
5555 const teardownDropTargets = installPluginDropTargets();
5556 const cardCallbacks = {
5557 onOpen: (slug, hint) => {
5558 if (!flyoutEl) {
5559 return;
5560 }
5561 openDetailFlyout(flyoutEl, slug, hint, {
5562 getInstalled: (s) => state.installed.get(s),
5563 onPluginInstalled: async (pluginFile, slug2) => {
5564 await refreshInstalled();
5565 const card = state.cardsBySlug.get(slug2);
5566 const plugin = state.plugins.find((p) => p.slug === slug2);
5567 if (card && plugin) {
5568 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5569 }
5570 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5571 source: SOURCE$2,
5572 plugin: pluginFile ?? slug2,
5573 action: "install"
5574 });
5575 if (pluginFile) {
5576 console.log("[plugins-window] installed", pluginFile);
5577 }
5578 },
5579 onPluginActivated: (updated) => {
5580 state.installed.set(indexKeyFor$1(updated), updated);
5581 const card = state.cardsBySlug.get(updated.textdomain ?? "");
5582 const plugin = state.plugins.find(
5583 (p) => p.slug === (updated.textdomain ?? "")
5584 );
5585 if (card && plugin) {
5586 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5587 }
5588 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5589 source: SOURCE$2,
5590 plugin: updated.plugin,
5591 action: "activate"
5592 });
5593 },
5594 onPluginDeactivated: (updated) => {
5595 state.installed.set(indexKeyFor$1(updated), updated);
5596 const card = state.cardsBySlug.get(updated.textdomain ?? "");
5597 const plugin = state.plugins.find(
5598 (p) => p.slug === (updated.textdomain ?? "")
5599 );
5600 if (card && plugin) {
5601 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5602 }
5603 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5604 source: SOURCE$2,
5605 plugin: updated.plugin,
5606 action: "deactivate"
5607 });
5608 },
5609 onPluginDeleted: (deleted) => {
5610 const key = indexKeyFor$1(deleted);
5611 state.installed.delete(key);
5612 const card = state.cardsBySlug.get(deleted.textdomain ?? "");
5613 const plugin = state.plugins.find(
5614 (p) => p.slug === (deleted.textdomain ?? "")
5615 );
5616 if (card && plugin) {
5617 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5618 }
5619 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5620 source: SOURCE$2,
5621 plugin: deleted.plugin,
5622 action: "delete"
5623 });
5624 }
5625 });
5626 },
5627 onInstall: async (plugin, card) => {
5628 const cta = card.querySelector("[data-plugin-card-cta]");
5629 const ctaOriginalText = cta?.textContent ?? "";
5630 cta?.setAttribute("busy", "");
5631 cta?.setAttribute("disabled", "");
5632 if (cta) {
5633 cta.textContent = __("Installing…", "desktop-mode");
5634 }
5635 try {
5636 await installPluginBySlug(plugin.slug);
5637 await refreshInstalled();
5638 toast$2(
5639 sprintf(
5640 /* translators: %s: plugin name */
5641 __("Installed %s.", "desktop-mode"),
5642 plugin.name
5643 )
5644 );
5645 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5646 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5647 source: SOURCE$2,
5648 plugin: plugin.slug,
5649 action: "install"
5650 });
5651 void refreshFrameworkMenu();
5652 } catch (err) {
5653 cta?.removeAttribute("busy");
5654 cta?.removeAttribute("disabled");
5655 if (cta) {
5656 cta.textContent = ctaOriginalText;
5657 }
5658 toast$2(
5659 sprintf(
5660 /* translators: %s: error message */
5661 __("Install failed: %s", "desktop-mode"),
5662 describe$1(err)
5663 ),
5664 6e3
5665 );
5666 }
5667 },
5668 onActivate: async (installed, card) => {
5669 const cta = card.querySelector("[data-plugin-card-cta]");
5670 const ctaOriginalText = cta?.textContent ?? "";
5671 cta?.setAttribute("busy", "");
5672 cta?.setAttribute("disabled", "");
5673 if (cta) {
5674 cta.textContent = __("Activating…", "desktop-mode");
5675 }
5676 try {
5677 const updated = await activateInstalledPlugin(installed);
5678 state.installed.set(indexKeyFor$1(updated), updated);
5679 toast$2(
5680 sprintf(
5681 /* translators: %s: plugin name */
5682 __("%s activated.", "desktop-mode"),
5683 updated.name || updated.plugin
5684 )
5685 );
5686 const plugin = state.plugins.find(
5687 (p) => p.slug === (updated.textdomain ?? "")
5688 );
5689 if (plugin) {
5690 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5691 }
5692 broadcast(PLUGINS_CHANGED_TOPIC$2, {
5693 source: SOURCE$2,
5694 plugin: updated.plugin,
5695 action: "activate"
5696 });
5697 void refreshFrameworkMenu();
5698 } catch (err) {
5699 cta?.removeAttribute("busy");
5700 cta?.removeAttribute("disabled");
5701 if (cta) {
5702 cta.textContent = ctaOriginalText;
5703 }
5704 toast$2(
5705 sprintf(
5706 /* translators: %s: error message */
5707 __("Activation failed: %s", "desktop-mode"),
5708 describe$1(err)
5709 ),
5710 6e3
5711 );
5712 }
5713 }
5714 };
5715 const observer = new IntersectionObserver(
5716 (entries) => {
5717 for (const entry of entries) {
5718 if (entry.isIntersecting) {
5719 void loadMore();
5720 }
5721 }
5722 },
5723 { root: gallery, rootMargin: "240px", threshold: 0 }
5724 );
5725 observer.observe(sentinel);
5726 void refreshInstalled();
5727 void resetAndLoad();
5728 async function refreshInstalled() {
5729 try {
5730 const rows = await fetchInstalledPlugins();
5731 state.installed = new Map(
5732 rows.map((r) => [indexKeyFor$1(r), r])
5733 );
5734 for (const [slug, card] of state.cardsBySlug) {
5735 const plugin = state.plugins.find((p) => p.slug === slug);
5736 if (plugin) {
5737 repaintCardCta(card, plugin, state.installed, cardCallbacks);
5738 }
5739 }
5740 } catch {
5741 }
5742 }
5743 async function resetAndLoad() {
5744 state.page = 1;
5745 state.totalPages = 0;
5746 state.exhausted = false;
5747 state.plugins = [];
5748 state.cardsBySlug.clear();
5749 gallery.replaceChildren();
5750 for (let i = 0; i < 6; i++) {
5751 gallery.appendChild(buildSkeletonCard$1());
5752 }
5753 gallery.appendChild(sentinel);
5754 await loadMore();
5755 }
5756 const inflightSkeletons = [];
5757 function showInflightLoader() {
5758 if (inflightSkeletons.length > 0) {
5759 return;
5760 }
5761 for (let i = 0; i < 4; i++) {
5762 const skel = buildSkeletonCard$1();
5763 gallery.insertBefore(skel, sentinel);
5764 inflightSkeletons.push(skel);
5765 }
5766 }
5767 function clearInflightLoader() {
5768 for (const skel of inflightSkeletons) {
5769 skel.remove();
5770 }
5771 inflightSkeletons.length = 0;
5772 }
5773 async function loadMore() {
5774 if (state.loading || state.exhausted) {
5775 return;
5776 }
5777 state.loading = true;
5778 if (state.page > 1) {
5779 showInflightLoader();
5780 }
5781 try {
5782 const data = await browsePlugins({
5783 browse: state.search === "" ? state.filter : void 0,
5784 search: state.search === "" ? void 0 : state.search,
5785 page: state.page,
5786 perPage: 24
5787 });
5788 if (state.page === 1) {
5789 gallery.replaceChildren();
5790 gallery.appendChild(sentinel);
5791 }
5792 const info = data.info ?? {};
5793 if (typeof info.pages === "number" && info.pages > 0) {
5794 state.totalPages = info.pages;
5795 }
5796 const incoming = data.plugins ?? [];
5797 if (incoming.length === 0) {
5798 state.exhausted = true;
5799 if (state.page === 1) {
5800 showStatus(__("No plugins matched.", "desktop-mode"));
5801 }
5802 return;
5803 }
5804 for (const plugin of incoming) {
5805 if (!plugin?.slug) {
5806 continue;
5807 }
5808 if (state.cardsBySlug.has(plugin.slug)) {
5809 continue;
5810 }
5811 const card = buildCard(plugin, state.installed, cardCallbacks);
5812 makeCardDraggable(card, plugin);
5813 gallery.insertBefore(card, sentinel);
5814 state.cardsBySlug.set(plugin.slug, card);
5815 state.plugins.push(plugin);
5816 }
5817 state.page++;
5818 if (state.totalPages > 0 && state.page > state.totalPages) {
5819 state.exhausted = true;
5820 } else if (state.totalPages === 0 && incoming.length < 24) {
5821 state.exhausted = true;
5822 }
5823 hideStatus();
5824 } catch (err) {
5825 showStatus(
5826 sprintf(
5827 /* translators: %s: error message */
5828 __("Could not load plugins: %s", "desktop-mode"),
5829 describe$1(err)
5830 )
5831 );
5832 } finally {
5833 clearInflightLoader();
5834 state.loading = false;
5835 }
5836 }
5837 function showStatus(message) {
5838 status.hidden = false;
5839 status.textContent = message;
5840 }
5841 function hideStatus() {
5842 status.hidden = true;
5843 status.textContent = "";
5844 }
5845 const unsubscribePluginsChanged = subscribe(
5846 PLUGINS_CHANGED_TOPIC$2,
5847 (payload) => {
5848 if (payload?.source === SOURCE$2) {
5849 return;
5850 }
5851 void refreshInstalled();
5852 }
5853 );
5854 return () => {
5855 unsubscribePluginsChanged();
5856 observer.disconnect();
5857 bodyEl.removeEventListener("dragenter", onDragEnter);
5858 bodyEl.removeEventListener("dragleave", onDragLeave);
5859 bodyEl.removeEventListener("dragover", onDragOver);
5860 bodyEl.removeEventListener("drop", onDrop);
5861 dropOverlay.remove();
5862 teardownDropTargets();
5863 host.replaceChildren();
5864 };
5865 }
5866 function buildSkeletonCard$1() {
5867 const card = document.createElement("wpd-card");
5868 card.classList.add(
5869 "desktop-mode-plugins__card",
5870 "desktop-mode-plugins__card--skeleton"
5871 );
5872 card.setAttribute("aria-hidden", "true");
5873 for (let i = 0; i < 4; i++) {
5874 const line = document.createElement("span");
5875 line.className = "desktop-mode-plugins__skeleton-line";
5876 line.style.width = `${50 + i * 17 % 50}%`;
5877 card.appendChild(line);
5878 }
5879 return card;
5880 }
5881 function indexKeyFor$1(plugin) {
5882 return plugin.textdomain || plugin.plugin;
5883 }
5884 function describe$1(err) {
5885 if (err instanceof Error) {
5886 return err.message;
5887 }
5888 return String(err);
5889 }
5890 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 )}`;
5891 const _WpdRibbon = class _WpdRibbon extends Component {
5892 render() {
5893 return html`<span class="banner" part="banner"><slot></slot></span>`;
5894 }
5895 };
5896 _WpdRibbon.props = ["placement", "tone"];
5897 _WpdRibbon.styles = [styles$8];
5898 _WpdRibbon.help = {
5899 title: "Ribbon",
5900 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.",
5901 status: "experimental",
5902 since: "0.8.6",
5903 props: [
5904 {
5905 name: "placement",
5906 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
5907 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
5908 },
5909 {
5910 name: "tone",
5911 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
5912 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
5913 }
5914 ],
5915 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
5916 cssProps: [
5917 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
5918 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
5919 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
5920 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
5921 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
5922 { name: "--wpd-ribbon-fg", default: "#fff" },
5923 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
5924 { name: "--wpd-ribbon-padding", default: "4px 0" },
5925 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
5926 { name: "--wpd-ribbon-tracking", default: "0.06em" },
5927 { name: "--wpd-ribbon-z", default: "2" }
5928 ],
5929 example: html`
5930 <div
5931 style="position: relative; width: 240px; height: 120px;
5932 border: 1px solid #ccc; border-radius: 8px;
5933 padding: 16px; box-sizing: border-box;"
5934 >
5935 <wpd-ribbon>Featured</wpd-ribbon>
5936 Card body…
5937 </div>
5938 `
5939 };
5940 let WpdRibbon = _WpdRibbon;
5941 defineComponent("wpd-ribbon", WpdRibbon);
5942 const PLUGINS_CHANGED_TOPIC$1 = "desktop-mode.plugin.changed";
5943 const SOURCE$1 = "featured-view";
5944 function toast$1(message, duration = 3500) {
5945 const api2 = window.wp?.desktop;
5946 if (api2 && typeof api2.showToast === "function") {
5947 api2.showToast({ message, duration });
5948 return;
5949 }
5950 console.log("[plugins-window]", message);
5951 }
5952 function mountFeaturedView(host, flyoutEl) {
5953 host.replaceChildren();
5954 const state = {
5955 plugins: [],
5956 installed: /* @__PURE__ */ new Map(),
5957 cardsBySlug: /* @__PURE__ */ new Map(),
5958 loading: true
5959 };
5960 const intro = document.createElement("header");
5961 intro.className = "desktop-mode-plugins__featured-intro";
5962 const heading = document.createElement("h2");
5963 heading.className = "desktop-mode-plugins__featured-heading";
5964 heading.textContent = __("Made for Desktop Mode", "desktop-mode");
5965 const description = document.createElement("p");
5966 description.className = "desktop-mode-plugins__featured-blurb";
5967 description.textContent = __(
5968 "Plugins that extend Desktop Mode — desktop decorations, native windows, widgets, and other companions.",
5969 "desktop-mode"
5970 );
5971 intro.append(heading, description);
5972 const gallery = document.createElement("div");
5973 gallery.className = "desktop-mode-plugins__gallery";
5974 const status = document.createElement("p");
5975 status.className = "desktop-mode-plugins__gallery-status";
5976 status.hidden = true;
5977 host.append(intro, gallery, status);
5978 const cardCallbacks = {
5979 onOpen: (slug, hint) => {
5980 if (!flyoutEl) {
5981 return;
5982 }
5983 openDetailFlyout(flyoutEl, slug, hint, {
5984 getInstalled: (s) => state.installed.get(s),
5985 onPluginInstalled: async (pluginFile, slug2) => {
5986 await refreshInstalled();
5987 repaintSlugCard(slug2);
5988 broadcast(PLUGINS_CHANGED_TOPIC$1, {
5989 source: SOURCE$1,
5990 plugin: pluginFile ?? slug2,
5991 action: "install"
5992 });
5993 },
5994 onPluginActivated: (updated) => {
5995 state.installed.set(indexKeyFor(updated), updated);
5996 repaintSlugCard(updated.textdomain ?? "");
5997 broadcast(PLUGINS_CHANGED_TOPIC$1, {
5998 source: SOURCE$1,
5999 plugin: updated.plugin,
6000 action: "activate"
6001 });
6002 },
6003 onPluginDeactivated: (updated) => {
6004 state.installed.set(indexKeyFor(updated), updated);
6005 repaintSlugCard(updated.textdomain ?? "");
6006 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6007 source: SOURCE$1,
6008 plugin: updated.plugin,
6009 action: "deactivate"
6010 });
6011 },
6012 onPluginDeleted: (deleted) => {
6013 state.installed.delete(indexKeyFor(deleted));
6014 repaintSlugCard(deleted.textdomain ?? "");
6015 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6016 source: SOURCE$1,
6017 plugin: deleted.plugin,
6018 action: "delete"
6019 });
6020 }
6021 });
6022 },
6023 onInstall: async (plugin, card) => {
6024 const cta = card.querySelector("[data-plugin-card-cta]");
6025 const originalText = cta?.textContent ?? "";
6026 cta?.setAttribute("busy", "");
6027 cta?.setAttribute("disabled", "");
6028 if (cta) {
6029 cta.textContent = __("Installing…", "desktop-mode");
6030 }
6031 try {
6032 await installPluginBySlug(plugin.slug);
6033 await refreshInstalled();
6034 toast$1(
6035 sprintf(
6036 /* translators: %s: plugin name */
6037 __("Installed %s.", "desktop-mode"),
6038 plugin.name
6039 )
6040 );
6041 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6042 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6043 source: SOURCE$1,
6044 plugin: plugin.slug,
6045 action: "install"
6046 });
6047 void refreshFrameworkMenu();
6048 } catch (err) {
6049 cta?.removeAttribute("busy");
6050 cta?.removeAttribute("disabled");
6051 if (cta) {
6052 cta.textContent = originalText;
6053 }
6054 toast$1(
6055 sprintf(
6056 /* translators: %s: error message */
6057 __("Install failed: %s", "desktop-mode"),
6058 formatError(err)
6059 ),
6060 6e3
6061 );
6062 }
6063 },
6064 onActivate: async (installed, card) => {
6065 const cta = card.querySelector("[data-plugin-card-cta]");
6066 const originalText = cta?.textContent ?? "";
6067 cta?.setAttribute("busy", "");
6068 cta?.setAttribute("disabled", "");
6069 if (cta) {
6070 cta.textContent = __("Activating…", "desktop-mode");
6071 }
6072 try {
6073 const updated = await activateInstalledPlugin(installed);
6074 state.installed.set(indexKeyFor(updated), updated);
6075 toast$1(
6076 sprintf(
6077 /* translators: %s: plugin name */
6078 __("%s activated.", "desktop-mode"),
6079 updated.name || updated.plugin
6080 )
6081 );
6082 const plugin = state.plugins.find(
6083 (p) => p.slug === (updated.textdomain ?? "")
6084 );
6085 if (plugin) {
6086 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6087 }
6088 broadcast(PLUGINS_CHANGED_TOPIC$1, {
6089 source: SOURCE$1,
6090 plugin: updated.plugin,
6091 action: "activate"
6092 });
6093 void refreshFrameworkMenu();
6094 } catch (err) {
6095 cta?.removeAttribute("busy");
6096 cta?.removeAttribute("disabled");
6097 if (cta) {
6098 cta.textContent = originalText;
6099 }
6100 toast$1(
6101 sprintf(
6102 /* translators: %s: error message */
6103 __("Activation failed: %s", "desktop-mode"),
6104 formatError(err)
6105 ),
6106 6e3
6107 );
6108 }
6109 }
6110 };
6111 void load();
6112 async function load() {
6113 state.loading = true;
6114 paintSkeletons();
6115 try {
6116 const [featured, installed] = await Promise.all([
6117 fetchFeaturedPlugins(),
6118 fetchInstalledPlugins().catch(() => [])
6119 ]);
6120 state.installed = new Map(
6121 installed.map((r) => [indexKeyFor(r), r])
6122 );
6123 state.plugins = featured.plugins ?? [];
6124 renderGallery();
6125 if (state.plugins.length === 0) {
6126 showStatus(__("No featured plugins yet.", "desktop-mode"));
6127 } else {
6128 hideStatus();
6129 }
6130 } catch (err) {
6131 gallery.replaceChildren();
6132 showStatus(
6133 sprintf(
6134 /* translators: %s: error message */
6135 __("Could not load featured plugins: %s", "desktop-mode"),
6136 formatError(err)
6137 )
6138 );
6139 } finally {
6140 state.loading = false;
6141 }
6142 }
6143 function paintSkeletons() {
6144 gallery.replaceChildren();
6145 state.cardsBySlug.clear();
6146 for (let i = 0; i < 3; i++) {
6147 gallery.appendChild(buildSkeletonCard());
6148 }
6149 }
6150 function renderGallery() {
6151 gallery.replaceChildren();
6152 state.cardsBySlug.clear();
6153 for (const plugin of state.plugins) {
6154 if (!plugin?.slug) {
6155 continue;
6156 }
6157 const card = buildCard(plugin, state.installed, cardCallbacks);
6158 if (plugin.featured) {
6159 card.classList.add("desktop-mode-plugins__card--featured");
6160 const ribbon = document.createElement("wpd-ribbon");
6161 ribbon.textContent = __("Featured", "desktop-mode");
6162 card.prepend(ribbon);
6163 }
6164 makeCardDraggable(card, plugin);
6165 gallery.appendChild(card);
6166 state.cardsBySlug.set(plugin.slug, card);
6167 }
6168 }
6169 function repaintSlugCard(slug) {
6170 if (!slug) {
6171 return;
6172 }
6173 const card = state.cardsBySlug.get(slug);
6174 const plugin = state.plugins.find((p) => p.slug === slug);
6175 if (card && plugin) {
6176 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6177 }
6178 }
6179 async function refreshInstalled() {
6180 try {
6181 const rows = await fetchInstalledPlugins();
6182 state.installed = new Map(
6183 rows.map((r) => [indexKeyFor(r), r])
6184 );
6185 for (const [slug, card] of state.cardsBySlug) {
6186 const plugin = state.plugins.find((p) => p.slug === slug);
6187 if (plugin) {
6188 repaintCardCta(card, plugin, state.installed, cardCallbacks);
6189 }
6190 }
6191 } catch {
6192 }
6193 }
6194 function showStatus(message) {
6195 status.hidden = false;
6196 status.textContent = message;
6197 }
6198 function hideStatus() {
6199 status.hidden = true;
6200 status.textContent = "";
6201 }
6202 const unsubscribePluginsChanged = subscribe(
6203 PLUGINS_CHANGED_TOPIC$1,
6204 (payload) => {
6205 if (payload?.source === SOURCE$1) {
6206 return;
6207 }
6208 void refreshInstalled();
6209 }
6210 );
6211 return () => {
6212 unsubscribePluginsChanged();
6213 host.replaceChildren();
6214 };
6215 }
6216 function buildSkeletonCard() {
6217 const card = document.createElement("wpd-card");
6218 card.classList.add(
6219 "desktop-mode-plugins__card",
6220 "desktop-mode-plugins__card--skeleton"
6221 );
6222 card.setAttribute("aria-hidden", "true");
6223 for (let i = 0; i < 4; i++) {
6224 const line = document.createElement("span");
6225 line.className = "desktop-mode-plugins__skeleton-line";
6226 line.style.width = `${50 + i * 17 % 50}%`;
6227 card.appendChild(line);
6228 }
6229 return card;
6230 }
6231 function indexKeyFor(plugin) {
6232 return plugin.textdomain || plugin.plugin;
6233 }
6234 function formatError(err) {
6235 if (err instanceof Error) {
6236 return err.message;
6237 }
6238 return String(err);
6239 }
6240 const queue = [];
6241 let inFlight = false;
6242 function enqueueUpdateJob(run) {
6243 return new Promise((resolve, reject) => {
6244 queue.push({
6245 run,
6246 resolve,
6247 reject
6248 });
6249 void drain();
6250 });
6251 }
6252 async function drain() {
6253 if (inFlight) {
6254 return;
6255 }
6256 const job = queue.shift();
6257 if (!job) {
6258 return;
6259 }
6260 inFlight = true;
6261 try {
6262 const value = await job.run();
6263 job.resolve(value);
6264 } catch (err) {
6265 job.reject(err);
6266 } finally {
6267 inFlight = false;
6268 void Promise.resolve().then(drain);
6269 }
6270 }
6271 const WP_ORG_ASSET_RE = /^(https:\/\/ps\.w\.org\/[a-z0-9-]+\/assets\/)icon\.svg$/i;
6272 function buildCandidates(initialUrl) {
6273 const match = initialUrl.match(WP_ORG_ASSET_RE);
6274 if (!match) {
6275 return [initialUrl];
6276 }
6277 const base = match[1];
6278 return [
6279 initialUrl,
6280 base + "icon-256x256.png",
6281 base + "icon-256x256.gif",
6282 base + "icon-128x128.png",
6283 base + "icon-128x128.gif"
6284 ];
6285 }
6286 function attachIconFallback(img, initialUrl, onExhausted) {
6287 const candidates = buildCandidates(initialUrl);
6288 let index = 0;
6289 img.addEventListener("error", () => {
6290 index += 1;
6291 if (index < candidates.length) {
6292 img.src = candidates[index];
6293 return;
6294 }
6295 onExhausted();
6296 });
6297 return candidates[0];
6298 }
6299 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 )}`;
6300 const _WpdChip = class _WpdChip extends Component {
6301 constructor() {
6302 super(...arguments);
6303 this._onHostKeyDown = (e) => {
6304 const dismissible = this.dismissible !== null;
6305 if (!dismissible) {
6306 return;
6307 }
6308 if (e.key === "Backspace" || e.key === "Delete") {
6309 e.preventDefault();
6310 const disabled = this.disabled !== null;
6311 if (disabled) {
6312 return;
6313 }
6314 const label = this.label ?? "";
6315 this.emit("wpd-chip-dismiss", { label });
6316 }
6317 };
6318 }
6319 connectedCallback() {
6320 super.connectedCallback();
6321 this.addEventListener("keydown", this._onHostKeyDown);
6322 }
6323 disconnectedCallback() {
6324 this.removeEventListener("keydown", this._onHostKeyDown);
6325 }
6326 render() {
6327 const label = this.label ?? "";
6328 const dismissible = this.dismissible !== null;
6329 const disabled = this.disabled !== null;
6330 return html`
6331 <span part="chip" class="wpd-chip">
6332 <span class="wpd-chip__icon">
6333 <slot name="icon"></slot>
6334 </span>
6335 <span class="wpd-chip__label">
6336 ${label === "" ? html`<slot></slot>` : label}
6337 </span>
6338 ${dismissible ? html`
6339 <button
6340 part="dismiss"
6341 class="wpd-chip__dismiss"
6342 type="button"
6343 aria-label=${`Remove ${label || "chip"}`}
6344 ?disabled=${disabled}
6345 @click=${(e) => this._onDismiss(e)}
6346 >
6347 ${_iconCross()}
6348 </button>
6349 ` : html``}
6350 </span>
6351 `;
6352 }
6353 _onDismiss(e) {
6354 e.stopPropagation();
6355 const disabled = this.disabled !== null;
6356 if (disabled) {
6357 return;
6358 }
6359 const label = this.label ?? "";
6360 this.emit("wpd-chip-dismiss", { label });
6361 }
6362 };
6363 _WpdChip.props = [
6364 "label",
6365 "tone",
6366 "size",
6367 "dismissible",
6368 "disabled",
6369 "pending"
6370 ];
6371 _WpdChip.styles = [styles$7];
6372 _WpdChip.help = {
6373 title: "Chip",
6374 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.",
6375 status: "experimental",
6376 since: "0.8.0",
6377 props: [
6378 {
6379 name: "label",
6380 type: "string",
6381 description: "Visible text. Falls back to the default slot when omitted."
6382 },
6383 {
6384 name: "tone",
6385 type: "'neutral' | 'accent' | 'positive' | 'warning' | 'danger'",
6386 default: "neutral",
6387 description: "Color variant. Mirrors <wpd-badge> tones."
6388 },
6389 {
6390 name: "size",
6391 type: "'default' | 'compact'",
6392 default: "default",
6393 description: "Vertical density. Compact halves horizontal padding for dense lists."
6394 },
6395 {
6396 name: "dismissible",
6397 type: "boolean attribute",
6398 description: "Renders a trailing × button. Click / Enter / Space emits wpd-chip-dismiss."
6399 },
6400 {
6401 name: "disabled",
6402 type: "boolean attribute",
6403 description: "Visually mutes the chip and blocks the dismiss button. Useful while a parent is mid-update."
6404 },
6405 {
6406 name: "pending",
6407 type: "boolean attribute",
6408 description: "Renders a subtle pulse animation while a REST mutation is in flight. Auto-applied by <wpd-tag-input>; safe to set by hand."
6409 }
6410 ],
6411 slots: [
6412 { name: "(default)", description: "Fallback label when `label` is unset." },
6413 {
6414 name: "icon",
6415 description: "Leading icon (Dashicon, SVG, image). Inherits text color."
6416 }
6417 ],
6418 parts: [
6419 { name: "chip", description: "The pill container." },
6420 {
6421 name: "dismiss",
6422 description: "The trailing × button (when `dismissible`)."
6423 }
6424 ],
6425 events: [
6426 {
6427 name: "wpd-chip-dismiss",
6428 description: "Fires when the dismiss button is activated. Detail carries the chip's label so a delegated listener can act without DOM walking.",
6429 detail: "{ label: string }"
6430 }
6431 ],
6432 cssProps: [
6433 { name: "--wpd-chip-bg", description: "Background color." },
6434 { name: "--wpd-chip-fg", description: "Text color." },
6435 { name: "--wpd-chip-border", description: "Border shorthand." },
6436 {
6437 name: "--wpd-chip-padding",
6438 description: "Padding shorthand.",
6439 default: "2px 8px"
6440 },
6441 {
6442 name: "--wpd-chip-radius",
6443 description: "Corner radius.",
6444 default: "999px"
6445 },
6446 {
6447 name: "--wpd-chip-label-max",
6448 description: "Max width of the inner label before ellipsis.",
6449 default: "220px"
6450 }
6451 ],
6452 example: html`
6453 <wpd-cluster gap="6">
6454 <wpd-chip label="Neutral"></wpd-chip>
6455 <wpd-chip label="Accent" tone="accent"></wpd-chip>
6456 <wpd-chip label="Positive" tone="positive"></wpd-chip>
6457 <wpd-chip label="Warning" tone="warning"></wpd-chip>
6458 <wpd-chip label="Danger" tone="danger"></wpd-chip>
6459 <wpd-chip label="Dismissible" dismissible></wpd-chip>
6460 </wpd-cluster>
6461 `
6462 };
6463 let WpdChip = _WpdChip;
6464 defineComponent("wpd-chip", WpdChip);
6465 function _iconCross() {
6466 return html`
6467 <svg
6468 viewBox="0 0 12 12"
6469 width="10"
6470 height="10"
6471 aria-hidden="true"
6472 focusable="false"
6473 fill="none"
6474 stroke="currentColor"
6475 stroke-width="1.5"
6476 stroke-linecap="round"
6477 >
6478 <path d="M3 3 L9 9 M9 3 L3 9" />
6479 </svg>
6480 `;
6481 }
6482 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}`;
6483 const _WpdCluster = class _WpdCluster extends Component {
6484 render() {
6485 const gap = this.gap;
6486 const justify = this.justify;
6487 const align = this.align;
6488 const gapPx = gap && /^\d+$/.test(gap) ? `${gap}px` : "";
6489 if (gapPx) {
6490 this.style.setProperty("--wpd-cluster-gap", gapPx);
6491 }
6492 if (justify) {
6493 this.style.setProperty("--wpd-cluster-justify", justify);
6494 }
6495 if (align) {
6496 this.style.setProperty("--wpd-cluster-align", align);
6497 }
6498 return html`<slot></slot>`;
6499 }
6500 };
6501 _WpdCluster.props = ["gap", "justify", "align"];
6502 _WpdCluster.styles = [styles$6];
6503 _WpdCluster.help = {
6504 title: "Cluster",
6505 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.",
6506 status: "stable",
6507 since: "0.5.0",
6508 props: [
6509 {
6510 name: "gap",
6511 type: "integer (px)",
6512 default: "8",
6513 description: "Space between children."
6514 },
6515 {
6516 name: "justify",
6517 type: "'start' | 'center' | 'end' | 'space-between' | 'space-around'",
6518 default: "start",
6519 description: "Main-axis alignment (justify-content)."
6520 },
6521 {
6522 name: "align",
6523 type: "'start' | 'center' | 'end' | 'stretch' | 'baseline'",
6524 default: "center",
6525 description: "Cross-axis alignment (align-items)."
6526 }
6527 ],
6528 slots: [
6529 { name: "(default)", description: "Inline children." }
6530 ],
6531 cssProps: [
6532 { name: "--wpd-cluster-gap", default: "8px" },
6533 { name: "--wpd-cluster-justify", default: "start" },
6534 { name: "--wpd-cluster-align", default: "center" }
6535 ],
6536 example: html`
6537 <wpd-cluster gap="8" justify="end">
6538 <wpd-button>Cancel</wpd-button>
6539 <wpd-button variant="primary">Save</wpd-button>
6540 </wpd-cluster>
6541 `
6542 };
6543 let WpdCluster = _WpdCluster;
6544 defineComponent("wpd-cluster", WpdCluster);
6545 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}`;
6546 const _WpdStack = class _WpdStack extends Component {
6547 render() {
6548 const gap = this.gap;
6549 const align = this.align;
6550 const padding = this.padding;
6551 const gapPx = gap && /^\d+$/.test(gap) ? `${gap}px` : "";
6552 if (gapPx) {
6553 this.style.setProperty("--wpd-stack-gap", gapPx);
6554 }
6555 if (align) {
6556 this.style.setProperty("--wpd-stack-align", align);
6557 }
6558 if (padding !== null && /^\d+$/.test(padding)) {
6559 this.style.setProperty("--wpd-stack-padding", `${padding}px`);
6560 }
6561 return html`<slot></slot>`;
6562 }
6563 };
6564 _WpdStack.props = ["gap", "align", "padding"];
6565 _WpdStack.styles = [styles$5];
6566 _WpdStack.help = {
6567 title: "Stack",
6568 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.',
6569 status: "stable",
6570 since: "0.5.0",
6571 props: [
6572 {
6573 name: "gap",
6574 type: "integer (px)",
6575 default: "12",
6576 description: "Space between children."
6577 },
6578 {
6579 name: "align",
6580 type: "'start' | 'center' | 'end' | 'stretch'",
6581 default: "stretch",
6582 description: "Cross-axis alignment (align-items)."
6583 },
6584 {
6585 name: "padding",
6586 type: "integer (px)",
6587 default: "0",
6588 description: "Inset padding on every side. Pass 0 for edge-to-edge."
6589 }
6590 ],
6591 slots: [
6592 { name: "(default)", description: "Stacked children." }
6593 ],
6594 cssProps: [
6595 { name: "--wpd-stack-gap", default: "12px" },
6596 { name: "--wpd-stack-align", default: "stretch" },
6597 { name: "--wpd-stack-padding", default: "0" }
6598 ],
6599 example: html`
6600 <wpd-stack gap="12">
6601 <wpd-section heading="Foo">First</wpd-section>
6602 <wpd-section heading="Bar">Second</wpd-section>
6603 </wpd-stack>
6604 `
6605 };
6606 let WpdStack = _WpdStack;
6607 defineComponent("wpd-stack", WpdStack);
6608 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}`;
6609 const _WpdGrid = class _WpdGrid extends Component {
6610 render() {
6611 const columns = this.columns;
6612 const rows = this.rows;
6613 const gap = this.gap;
6614 const cg = this["column-gap"];
6615 const rg = this["row-gap"];
6616 if (columns && /^\d+$/.test(columns)) {
6617 this.style.setProperty(
6618 "--wpd-grid-columns",
6619 `repeat(${columns}, minmax(0, 1fr))`
6620 );
6621 }
6622 if (rows && /^\d+$/.test(rows)) {
6623 this.style.setProperty(
6624 "--wpd-grid-rows",
6625 `repeat(${rows}, minmax(0, 1fr))`
6626 );
6627 }
6628 if (gap && /^\d+$/.test(gap)) {
6629 this.style.setProperty("--wpd-grid-gap", `${gap}px`);
6630 }
6631 if (cg && /^\d+$/.test(cg)) {
6632 this.style.setProperty("--wpd-grid-column-gap", `${cg}px`);
6633 }
6634 if (rg && /^\d+$/.test(rg)) {
6635 this.style.setProperty("--wpd-grid-row-gap", `${rg}px`);
6636 }
6637 return html`<slot></slot>`;
6638 }
6639 };
6640 _WpdGrid.props = ["columns", "rows", "gap", "column-gap", "row-gap"];
6641 _WpdGrid.styles = [styles$4];
6642 _WpdGrid.help = {
6643 title: "Grid",
6644 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.',
6645 status: "stable",
6646 since: "0.5.0",
6647 props: [
6648 {
6649 name: "columns",
6650 type: "integer",
6651 default: "1",
6652 description: "Number of equal-width columns (repeat(N, minmax(0, 1fr)))."
6653 },
6654 {
6655 name: "rows",
6656 type: "integer",
6657 description: "Optional fixed row count. Omit for content-driven sizing."
6658 },
6659 { name: "gap", type: "integer (px)", description: "Cell spacing on both axes." },
6660 { name: "column-gap", type: "integer (px)", description: "x-axis override." },
6661 { name: "row-gap", type: "integer (px)", description: "y-axis override." }
6662 ],
6663 slots: [
6664 { name: "(default)", description: "Grid children." }
6665 ],
6666 cssProps: [
6667 { name: "--wpd-grid-columns" },
6668 { name: "--wpd-grid-rows" },
6669 { name: "--wpd-grid-gap" },
6670 { name: "--wpd-grid-column-gap" },
6671 { name: "--wpd-grid-row-gap" }
6672 ],
6673 example: html`
6674 <wpd-grid columns="4" gap="8">
6675 <wpd-button>7</wpd-button>
6676 <wpd-button>8</wpd-button>
6677 <wpd-button>9</wpd-button>
6678 <wpd-button variant="primary">÷</wpd-button>
6679 <wpd-button>4</wpd-button>
6680 <wpd-button>5</wpd-button>
6681 <wpd-button>6</wpd-button>
6682 <wpd-button variant="primary">×</wpd-button>
6683 </wpd-grid>
6684 `
6685 };
6686 let WpdGrid = _WpdGrid;
6687 defineComponent("wpd-grid", WpdGrid);
6688 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}}`;
6689 const WPD_SPINNER_PRESETS = Object.freeze({
6690 classic: {
6691 sp1: 12,
6692 sp2: 24,
6693 sp3: 40,
6694 a1: 28,
6695 a2: 15,
6696 a3: 8,
6697 gap: 4,
6698 dir2: 1,
6699 dir3: -1,
6700 pulse: "none",
6701 dots: 0
6702 },
6703 comet: {
6704 sp1: 8,
6705 sp2: 14,
6706 sp3: 26,
6707 a1: 50,
6708 a2: 28,
6709 a3: 12,
6710 gap: 3,
6711 dir2: 1,
6712 dir3: 1,
6713 pulse: "none",
6714 dots: 5
6715 },
6716 orbit: {
6717 sp1: 10,
6718 sp2: 10,
6719 sp3: 32,
6720 a1: 50,
6721 a2: 50,
6722 a3: 8,
6723 gap: 5,
6724 dir2: -1,
6725 dir3: -1,
6726 pulse: "opacity",
6727 dots: 3
6728 },
6729 pulse: {
6730 sp1: 6,
6731 sp2: 18,
6732 sp3: 30,
6733 a1: 20,
6734 a2: 12,
6735 a3: 6,
6736 gap: 4,
6737 dir2: 1,
6738 dir3: -1,
6739 pulse: "both",
6740 dots: 8
6741 }
6742 });
6743 const CX = 61.26;
6744 const CY = 61.26;
6745 const DISC_R = 58.453;
6746 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"/>';
6747 const _WpdSpinner = class _WpdSpinner extends Component {
6748 constructor() {
6749 super(...arguments);
6750 this._paintScheduled = false;
6751 }
6752 connectedCallback() {
6753 super.connectedCallback();
6754 this._schedulePaint();
6755 }
6756 render() {
6757 return html`<div class="root" part="root"></div>`;
6758 }
6759 requestUpdate() {
6760 super.requestUpdate();
6761 this._schedulePaint();
6762 }
6763 _schedulePaint() {
6764 if (this._paintScheduled || !this.isConnected) {
6765 return;
6766 }
6767 this._paintScheduled = true;
6768 queueMicrotask(() => {
6769 this._paintScheduled = false;
6770 if (!this.isConnected) {
6771 return;
6772 }
6773 this._paint();
6774 });
6775 }
6776 _paint() {
6777 this._syncCssVars();
6778 const root = this.shadowRoot?.querySelector(
6779 ".root"
6780 );
6781 if (!root) {
6782 return;
6783 }
6784 root.innerHTML = this._buildSvg();
6785 }
6786 /**
6787 * Reflect the color / accent / size attributes onto CSS custom
6788 * properties on the host. Removing the attribute clears the var
6789 * so the default cascades back in.
6790 */
6791 _syncCssVars() {
6792 const sync = (attr, varName, transform) => {
6793 const v = this.getAttribute(attr);
6794 if (v === null) {
6795 this.style.removeProperty(varName);
6796 } else {
6797 this.style.setProperty(
6798 varName,
6799 transform ? transform(v) : v
6800 );
6801 }
6802 };
6803 sync("color", "--wpd-spinner-color");
6804 sync("accent", "--wpd-spinner-accent");
6805 sync(
6806 "size",
6807 "--wpd-spinner-size",
6808 (v) => /^-?\d+(\.\d+)?$/.test(v.trim()) ? `${v}px` : v
6809 );
6810 }
6811 _effectiveConfig() {
6812 const presetName = this.getAttribute("preset") ?? "classic";
6813 const preset = WPD_SPINNER_PRESETS[presetName] ?? WPD_SPINNER_PRESETS.classic;
6814 const num = (attr, fallback) => {
6815 const v = this.getAttribute(attr);
6816 if (v === null) {
6817 return fallback;
6818 }
6819 const n = parseFloat(v);
6820 return Number.isFinite(n) ? n : fallback;
6821 };
6822 const dir = (attr, fallback) => {
6823 const v = this.getAttribute(attr);
6824 if (v === null) {
6825 return fallback;
6826 }
6827 const lc = v.toLowerCase();
6828 if (lc === "-1" || lc === "ccw" || lc === "reverse") {
6829 return -1;
6830 }
6831 return 1;
6832 };
6833 const pulse = () => {
6834 const v = this.getAttribute("pulse");
6835 if (v === "scale" || v === "opacity" || v === "both" || v === "none") {
6836 return v;
6837 }
6838 return preset.pulse;
6839 };
6840 return {
6841 sp1: num("sp1", preset.sp1),
6842 sp2: num("sp2", preset.sp2),
6843 sp3: num("sp3", preset.sp3),
6844 a1: num("a1", preset.a1),
6845 a2: num("a2", preset.a2),
6846 a3: num("a3", preset.a3),
6847 gap: num("gap", preset.gap),
6848 dir2: dir("dir2", preset.dir2),
6849 dir3: dir("dir3", preset.dir3),
6850 pulse: pulse(),
6851 dots: Math.max(0, Math.floor(num("dots", preset.dots)))
6852 };
6853 }
6854 _buildSvg() {
6855 const cfg = this._effectiveConfig();
6856 const label = escAttr(this.getAttribute("label") ?? "Loading");
6857 const pad = cfg.gap * 3 + 14;
6858 const vbMin = -pad;
6859 const vbSize = 122.52 + pad * 2;
6860 const r1 = DISC_R + cfg.gap + 2;
6861 const r2 = r1 + cfg.gap + 2;
6862 const r3 = r2 + cfg.gap + 1.5;
6863 const ring1Anim = `animation: wpd-spinner-spin ${(cfg.sp1 / 10).toFixed(2)}s linear infinite`;
6864 const ring2Anim = `animation: wpd-spinner-spin ${(cfg.sp2 / 10).toFixed(2)}s linear infinite${cfg.dir2 < 0 ? " reverse" : ""}`;
6865 const ring3Anim = `animation: wpd-spinner-spin ${(cfg.sp3 / 10).toFixed(2)}s linear infinite${cfg.dir3 < 0 ? " reverse" : ""}`;
6866 const pspd = (cfg.sp1 * 1.8 / 10).toFixed(1);
6867 const ospd = (cfg.sp1 * 2.3 / 10).toFixed(1);
6868 let pulseStyle = "";
6869 if (cfg.pulse === "scale") {
6870 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite`;
6871 } else if (cfg.pulse === "opacity") {
6872 pulseStyle = `animation: wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
6873 } else if (cfg.pulse === "both") {
6874 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite, wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
6875 }
6876 let dotEls = "";
6877 if (cfg.dots > 0) {
6878 const dr = r3 + cfg.gap + 1;
6879 const dc2 = 2 * Math.PI * dr;
6880 const dsz = 1.6;
6881 const dotDur = (cfg.sp1 * 0.65 / 10).toFixed(2);
6882 for (let i = 0; i < cfg.dots; i++) {
6883 const offset = -(i / cfg.dots) * dc2;
6884 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"/>`;
6885 }
6886 }
6887 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>`;
6888 }
6889 };
6890 _WpdSpinner.props = [
6891 "preset",
6892 "size",
6893 "color",
6894 "accent",
6895 "sp1",
6896 "sp2",
6897 "sp3",
6898 "a1",
6899 "a2",
6900 "a3",
6901 "gap",
6902 "dir2",
6903 "dir3",
6904 "pulse",
6905 "dots",
6906 "label"
6907 ];
6908 _WpdSpinner.styles = [styles$3];
6909 _WpdSpinner.help = {
6910 title: "Spinner",
6911 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.",
6912 status: "experimental",
6913 since: "0.6.0",
6914 props: [
6915 {
6916 name: "preset",
6917 type: '"classic" | "comet" | "orbit" | "pulse"',
6918 default: "classic",
6919 description: "Visual personality. Every other attribute defaults to the preset's value and can be overridden individually."
6920 },
6921 {
6922 name: "size",
6923 type: "integer (px) or CSS length",
6924 default: "48",
6925 description: "Sets `--wpd-spinner-size`. Bare numbers are treated as px; pass a CSS length (e.g. `2em`) to opt into ems / rems."
6926 },
6927 {
6928 name: "color",
6929 type: "CSS color",
6930 description: "Disc + ring + dot color. Sets `--wpd-spinner-color`. Default inherits the WP admin theme color."
6931 },
6932 {
6933 name: "accent",
6934 type: "CSS color",
6935 default: "#fff",
6936 description: "Color of the W mark inside the disc. Sets `--wpd-spinner-accent`. Default white — change for dark-on-light or themed marks."
6937 },
6938 {
6939 name: "sp1, sp2, sp3",
6940 type: "integer (deciseconds)",
6941 description: "Per-ring rotation duration in tenths-of-a-second (12 → 1.2s). Higher = slower."
6942 },
6943 {
6944 name: "a1, a2, a3",
6945 type: "integer (0-100)",
6946 description: "Per-ring arc length as a percentage of the ring circumference."
6947 },
6948 {
6949 name: "gap",
6950 type: "integer",
6951 description: "Gap between concentric rings (units approximate to px at 120-viewport)."
6952 },
6953 {
6954 name: "dir2, dir3",
6955 type: '"1" | "-1" | "cw" | "ccw"',
6956 description: "Per-ring direction; ring 1 is always clockwise."
6957 },
6958 {
6959 name: "pulse",
6960 type: '"none" | "scale" | "opacity" | "both"',
6961 description: "Pulse animation applied to the disc + W mark."
6962 },
6963 {
6964 name: "dots",
6965 type: "integer",
6966 description: "Outer trailing dot count. Sensible values: 0, 3, 5, 8."
6967 },
6968 {
6969 name: "label",
6970 type: "string",
6971 default: "Loading",
6972 description: 'Accessible name for the SVG (`role="img"` + `aria-label`).'
6973 }
6974 ],
6975 cssProps: [
6976 { name: "--wpd-spinner-color", default: "var(--wp-admin-theme-color, #21759b)" },
6977 { name: "--wpd-spinner-accent", default: "#fff" },
6978 { name: "--wpd-spinner-size", default: "48px" }
6979 ],
6980 example: html`<wpd-spinner preset="comet" size="80"></wpd-spinner>`
6981 };
6982 let WpdSpinner = _WpdSpinner;
6983 function dasharray(r, pct) {
6984 const c = 2 * Math.PI * r;
6985 const visible = pct / 100 * c;
6986 const gap = c - visible;
6987 return `${visible.toFixed(2)} ${gap.toFixed(2)}`;
6988 }
6989 function escAttr(s) {
6990 return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6991 }
6992 defineComponent("wpd-spinner", WpdSpinner);
6993 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}`;
6994 let _cache = null;
6995 function parseCssContentToChar(raw) {
6996 let value = raw.trim();
6997 if (value === "") {
6998 return null;
6999 }
7000 if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
7001 value = value.slice(1, -1);
7002 }
7003 const escaped = value.match(/^\\([0-9a-f]{1,6})\s?$/i);
7004 if (escaped) {
7005 return String.fromCodePoint(parseInt(escaped[1], 16));
7006 }
7007 return value || null;
7008 }
7009 function buildMap() {
7010 const map = /* @__PURE__ */ new Map();
7011 if (typeof document === "undefined") {
7012 return map;
7013 }
7014 const sheets = Array.from(document.styleSheets ?? []);
7015 for (const sheet of sheets) {
7016 let rules = null;
7017 try {
7018 rules = sheet.cssRules;
7019 } catch {
7020 continue;
7021 }
7022 if (!rules) {
7023 continue;
7024 }
7025 for (const rule of Array.from(rules)) {
7026 const styleRule = rule;
7027 if (!styleRule || !styleRule.selectorText) {
7028 continue;
7029 }
7030 const match = styleRule.selectorText.match(
7031 /\.dashicons-([a-z0-9-]+)::?before/i
7032 );
7033 if (!match) {
7034 continue;
7035 }
7036 const content = styleRule.style?.content;
7037 if (!content) {
7038 continue;
7039 }
7040 const char = parseCssContentToChar(content);
7041 if (char) {
7042 map.set(match[1], char);
7043 }
7044 }
7045 }
7046 return map;
7047 }
7048 function resolveDashicon(name) {
7049 if (!_cache) {
7050 _cache = buildMap();
7051 }
7052 const slug = name.startsWith("dashicons-") ? name.slice("dashicons-".length) : name;
7053 return _cache.get(slug) ?? null;
7054 }
7055 function refreshDashiconCache() {
7056 _cache = buildMap();
7057 }
7058 let _scheduled = false;
7059 function primeOnLoad() {
7060 if (_scheduled || typeof window === "undefined") {
7061 return;
7062 }
7063 _scheduled = true;
7064 const refresh = () => {
7065 refreshDashiconCache();
7066 };
7067 if (document.readyState === "loading") {
7068 document.addEventListener("DOMContentLoaded", refresh, { once: true });
7069 }
7070 window.addEventListener("load", refresh, { once: true });
7071 }
7072 primeOnLoad();
7073 const _WpdIcon = class _WpdIcon extends Component {
7074 render() {
7075 const rawName = this.name || "";
7076 const slug = rawName.startsWith("dashicons-") ? rawName.slice("dashicons-".length) : rawName;
7077 const size = this.size;
7078 if (size && /^\d+$/.test(size)) {
7079 this.style.setProperty("--wpd-icon-size", `${size}px`);
7080 }
7081 const char = resolveDashicon(slug);
7082 if (char) {
7083 return html`<span
7084 class="wpd-icon__glyph wpd-icon__glyph--char dashicons dashicons-${slug}"
7085 aria-hidden="true"
7086 >${char}</span>`;
7087 }
7088 return html`<span
7089 class="wpd-icon__glyph dashicons dashicons-${slug}"
7090 aria-hidden="true"
7091 ></span>`;
7092 }
7093 };
7094 _WpdIcon.props = ["name", "size"];
7095 _WpdIcon.styles = [styles$2];
7096 _WpdIcon.help = {
7097 title: "Icon",
7098 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.',
7099 status: "stable",
7100 since: "0.5.0",
7101 props: [
7102 {
7103 name: "name",
7104 type: "string",
7105 description: "Dashicon identifier, with or without the `dashicons-` prefix."
7106 },
7107 {
7108 name: "size",
7109 type: "integer (px)",
7110 default: "16",
7111 description: "Glyph size in pixels."
7112 }
7113 ],
7114 cssProps: [
7115 { name: "--wpd-icon-size", default: "16px" }
7116 ],
7117 example: html`
7118 <wpd-cluster gap="8" align="center">
7119 <wpd-icon name="admin-post"></wpd-icon>
7120 <wpd-icon name="calculator" size="20"></wpd-icon>
7121 <wpd-icon name="dashicons-star-filled" size="32"></wpd-icon>
7122 </wpd-cluster>
7123 `
7124 };
7125 let WpdIcon = _WpdIcon;
7126 defineComponent("wpd-icon", WpdIcon);
7127 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}`;
7128 const _WpdEmptyState = class _WpdEmptyState extends Component {
7129 render() {
7130 const icon = this.icon || "";
7131 const heading = this.heading || "";
7132 const description = this.description || "";
7133 return html`
7134 ${icon ? html`<wpd-icon
7135 class="wpd-empty-state__icon"
7136 name=${icon}
7137 size="28"
7138 ></wpd-icon>` : null}
7139 <h3 class="wpd-empty-state__heading">${heading}</h3>
7140 <p class="wpd-empty-state__description">${description}</p>
7141 <div class="wpd-empty-state__cta">
7142 <slot name="cta"></slot>
7143 </div>
7144 <slot></slot>
7145 `;
7146 }
7147 };
7148 _WpdEmptyState.props = ["icon", "heading", "description"];
7149 _WpdEmptyState.styles = [styles$1];
7150 _WpdEmptyState.help = {
7151 title: "Empty state",
7152 summary: 'Centered placeholder for "nothing here yet" UI: icon + heading + description + optional CTA. A canonical shape so empty states look consistent across the shell.',
7153 status: "stable",
7154 since: "0.5.0",
7155 props: [
7156 {
7157 name: "icon",
7158 type: "string (dashicons slug)",
7159 description: "Dashicons identifier (with or without the dashicons- prefix)."
7160 },
7161 {
7162 name: "heading",
7163 type: "string",
7164 description: "Bold first line."
7165 },
7166 {
7167 name: "description",
7168 type: "string",
7169 description: "Secondary paragraph below the heading."
7170 }
7171 ],
7172 slots: [
7173 { name: "cta", description: "Call-to-action button row below the description." },
7174 { name: "(default)", description: "Any additional content rendered after the CTA." }
7175 ],
7176 cssProps: [
7177 { name: "--desktop-mode-text", description: "Heading colour." },
7178 { name: "--desktop-mode-muted", description: "Description colour." },
7179 { name: "--wpd-empty-state-fg" },
7180 { name: "--wpd-empty-state-icon-color" }
7181 ],
7182 example: html`
7183 <wpd-empty-state
7184 icon="admin-plugins"
7185 heading="No plugins installed yet"
7186 description="Install a plugin to see it here."
7187 >
7188 <wpd-button slot="cta" variant="primary">Browse plugins</wpd-button>
7189 </wpd-empty-state>
7190 `
7191 };
7192 let WpdEmptyState = _WpdEmptyState;
7193 defineComponent("wpd-empty-state", WpdEmptyState);
7194 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}}`;
7195 const _WpdRatingSummary = class _WpdRatingSummary extends Component {
7196 constructor() {
7197 super(...arguments);
7198 this._ratings = {};
7199 }
7200 /**
7201 * Per-star counts. Setting this triggers a re-render so consumers
7202 * can swap data without recreating the element.
7203 */
7204 get ratings() {
7205 return { ...this._ratings };
7206 }
7207 set ratings(next) {
7208 this._ratings = next ? { ...next } : {};
7209 this.requestUpdate();
7210 }
7211 render() {
7212 const rating = clamp01to100(numAttr(this, "rating"));
7213 const stars0to5 = rating / 100 * 5;
7214 const totalAttr = numAttr(this, "total");
7215 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);
7216 const fmt = new Intl.NumberFormat();
7217 const big = rating > 0 ? (rating / 100 * 5).toFixed(1) : "";
7218 return html`
7219 <div class="summary-card" role="img" aria-label=${ariaLabel(rating, total)}>
7220 <div class="summary">
7221 <div class="big">${big}</div>
7222 <div class="stars" aria-hidden="true">
7223 ${renderStarRow(stars0to5)}
7224 </div>
7225 <div class="total">
7226 ${total === 1 ? "1 rating" : `${fmt.format(total)} ratings`}
7227 </div>
7228 </div>
7229 <div class="bars">
7230 ${[5, 4, 3, 2, 1].map((star) => {
7231 const count = this._ratings[String(star)] ?? 0;
7232 const ratio = total === 0 ? 0 : count / total;
7233 return html`
7234 <div
7235 class="row"
7236 role="presentation"
7237 aria-label=${`${star} stars: ${fmt.format(count)}`}
7238 >
7239 <span class="row__label">
7240 ${star} ${filledStarSvg()}
7241 </span>
7242 <span class="row__track">
7243 <span
7244 class="row__fill"
7245 style=${`--ratio: ${ratio.toFixed(4)}`}
7246 ></span>
7247 </span>
7248 <span class="row__count">${fmt.format(count)}</span>
7249 </div>
7250 `;
7251 })}
7252 </div>
7253 </div>
7254 `;
7255 }
7256 };
7257 _WpdRatingSummary.props = ["rating", "total"];
7258 _WpdRatingSummary.styles = [styles];
7259 _WpdRatingSummary.help = {
7260 title: "Rating summary",
7261 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.",
7262 status: "experimental",
7263 since: "0.8.5",
7264 props: [
7265 {
7266 name: "rating",
7267 type: "number (0–100)",
7268 description: "Average rating on the wp.org 0–100 scale. Converted to a 0–5 display inside."
7269 },
7270 {
7271 name: "total",
7272 type: "number",
7273 description: "Total number of ratings. Optional — auto-summed from `ratings` when omitted."
7274 }
7275 ],
7276 cssProps: [
7277 { name: "--wpd-rating-fill", description: "Background of the bar fill." },
7278 { name: "--wpd-rating-track", description: "Background of the empty bar track." },
7279 { name: "--wpd-rating-star", description: "Color of filled stars." },
7280 { name: "--wpd-rating-star-empty", description: "Color of empty stars." },
7281 { name: "--wpd-rating-surface", description: "Card background." },
7282 { name: "--wpd-rating-border", description: "Card border color." },
7283 { name: "--wpd-rating-fg", description: "Primary text color." },
7284 { name: "--wpd-rating-fg-muted", description: "Secondary text color." }
7285 ],
7286 example: html`
7287 <wpd-rating-summary rating="92"></wpd-rating-summary>
7288 `
7289 };
7290 let WpdRatingSummary = _WpdRatingSummary;
7291 function numAttr(host, name) {
7292 const raw = host.getAttribute(name);
7293 if (raw === null || raw === "") {
7294 return 0;
7295 }
7296 const n = Number(raw);
7297 return Number.isFinite(n) ? n : 0;
7298 }
7299 function clamp01to100(n) {
7300 if (n < 0) {
7301 return 0;
7302 }
7303 if (n > 100) {
7304 return 100;
7305 }
7306 return n;
7307 }
7308 function ariaLabel(rating, total) {
7309 if (total === 0) {
7310 return "No ratings yet";
7311 }
7312 const stars = rating / 100 * 5;
7313 return `Average rating ${stars.toFixed(1)} out of 5, from ${total} ratings`;
7314 }
7315 function renderStarRow(stars0to5) {
7316 const full = Math.floor(stars0to5);
7317 const half = stars0to5 - full >= 0.5 ? 1 : 0;
7318 const empty = 5 - full - half;
7319 const list = [];
7320 for (let i = 0; i < full; i++) {
7321 list.push(filledStarSvg());
7322 }
7323 for (let i = 0; i < half; i++) {
7324 list.push(halfStarSvg());
7325 }
7326 for (let i = 0; i < empty; i++) {
7327 list.push(emptyStarSvg());
7328 }
7329 return list;
7330 }
7331 function filledStarSvg() {
7332 return html`
7333 <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
7334 <path
7335 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"
7336 />
7337 </svg>
7338 `;
7339 }
7340 function halfStarSvg() {
7341 return html`
7342 <svg viewBox="0 0 16 16" aria-hidden="true">
7343 <defs>
7344 <linearGradient id="wpd-half-star">
7345 <stop offset="50%" stop-color="currentColor" />
7346 <stop
7347 offset="50%"
7348 stop-color="currentColor"
7349 stop-opacity="0.22"
7350 />
7351 </linearGradient>
7352 </defs>
7353 <path
7354 fill="url(#wpd-half-star)"
7355 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"
7356 />
7357 </svg>
7358 `;
7359 }
7360 function emptyStarSvg() {
7361 return html`
7362 <svg
7363 class="empty"
7364 viewBox="0 0 16 16"
7365 fill="currentColor"
7366 aria-hidden="true"
7367 >
7368 <path
7369 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"
7370 />
7371 </svg>
7372 `;
7373 }
7374 defineComponent("wpd-rating-summary", WpdRatingSummary);
7375 const wpOrgCache = /* @__PURE__ */ new Map();
7376 const reviewsCache = /* @__PURE__ */ new Map();
7377 function buildInstalledDetail(row) {
7378 const root = document.createElement("div");
7379 root.className = "desktop-mode-plugins__detail";
7380 root.setAttribute("data-noclick", "");
7381 const style = document.createElement("style");
7382 style.textContent = PANEL_STYLES;
7383 root.appendChild(style);
7384 const slug = deriveSlug(row);
7385 root.appendChild(buildHero(row));
7386 const tabsHost = document.createElement("div");
7387 tabsHost.className = "desktop-mode-plugins__detail-tabs-wrap";
7388 root.appendChild(tabsHost);
7389 const body = document.createElement("div");
7390 body.className = "desktop-mode-plugins__detail-body";
7391 root.appendChild(body);
7392 const tabs = document.createElement("wpd-tabs");
7393 tabs.className = "desktop-mode-plugins__detail-tabs";
7394 tabs.setAttribute("value", "overview");
7395 const tabDefs = [
7396 { value: "overview", label: __("Overview", "desktop-mode"), show: true },
7397 { value: "details", label: __("Details", "desktop-mode"), show: true },
7398 { value: "changelog", label: __("Changelog", "desktop-mode"), show: !!slug },
7399 { value: "faq", label: __("FAQ", "desktop-mode"), show: !!slug },
7400 { value: "reviews", label: __("Reviews", "desktop-mode"), show: !!slug }
7401 ];
7402 for (const def of tabDefs) {
7403 if (!def.show) {
7404 continue;
7405 }
7406 const tab = document.createElement("wpd-tab");
7407 tab.setAttribute("value", def.value);
7408 tab.textContent = def.label;
7409 tabs.appendChild(tab);
7410 }
7411 tabsHost.appendChild(tabs);
7412 let info = slug ? wpOrgCache.get(slug) ?? null : null;
7413 let infoFetching = false;
7414 let active = "overview";
7415 const ensureInfo = () => {
7416 if (!slug || info || infoFetching) {
7417 return;
7418 }
7419 infoFetching = true;
7420 void (async () => {
7421 try {
7422 info = await fetchPluginInfo(slug);
7423 wpOrgCache.set(slug, info);
7424 if (root.isConnected) {
7425 paintActive();
7426 }
7427 } catch {
7428 if (root.isConnected) {
7429 paintActive();
7430 }
7431 } finally {
7432 infoFetching = false;
7433 }
7434 })();
7435 };
7436 const paintActive = () => {
7437 body.replaceChildren(renderTab(active, row, slug, info));
7438 };
7439 tabs.addEventListener("wpd-tab-change", (ev) => {
7440 const detail = ev.detail;
7441 active = detail?.value ?? "overview";
7442 if (slug && active !== "overview" && active !== "details") {
7443 ensureInfo();
7444 }
7445 paintActive();
7446 });
7447 paintActive();
7448 return root;
7449 }
7450 function buildHero(row, _slug) {
7451 const hero = document.createElement("div");
7452 hero.className = "desktop-mode-plugins__detail-hero";
7453 const inner = document.createElement("div");
7454 inner.className = "desktop-mode-plugins__detail-hero-inner";
7455 const iconTile = document.createElement("div");
7456 iconTile.className = "desktop-mode-plugins__detail-hero-icon";
7457 const iconUrl = row.desktop_mode_icon_url;
7458 if (iconUrl) {
7459 const img = document.createElement("img");
7460 img.alt = "";
7461 img.loading = "lazy";
7462 img.decoding = "async";
7463 img.src = attachIconFallback(img, iconUrl, () => {
7464 iconTile.replaceChildren(buildFallbackGlyph());
7465 });
7466 iconTile.appendChild(img);
7467 } else {
7468 iconTile.appendChild(buildFallbackGlyph());
7469 }
7470 const titleBlock = document.createElement("wpd-stack");
7471 titleBlock.setAttribute("gap", "6");
7472 titleBlock.className = "desktop-mode-plugins__detail-hero-text";
7473 const titleRow = document.createElement("wpd-cluster");
7474 titleRow.setAttribute("gap", "10");
7475 titleRow.setAttribute("align", "center");
7476 const title = document.createElement("h3");
7477 title.className = "desktop-mode-plugins__detail-title";
7478 title.textContent = row.name || row.plugin;
7479 titleRow.appendChild(title);
7480 if (row.version) {
7481 const ver = document.createElement("wpd-badge");
7482 ver.setAttribute("tone", "neutral");
7483 ver.setAttribute("no-dot", "");
7484 ver.textContent = sprintf(
7485 /* translators: %s: version number */
7486 __("v%s", "desktop-mode"),
7487 row.version
7488 );
7489 titleRow.appendChild(ver);
7490 }
7491 const isActive = row.status === "active" || row.status === "active-network";
7492 const statusBadge = document.createElement("wpd-badge");
7493 statusBadge.setAttribute("tone", isActive ? "success" : "neutral");
7494 statusBadge.textContent = isActive ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode");
7495 titleRow.appendChild(statusBadge);
7496 const update = row.desktop_mode_update_available;
7497 if (update?.available && update.new_version) {
7498 const upd = document.createElement("wpd-badge");
7499 upd.setAttribute("tone", "warning");
7500 upd.textContent = sprintf(
7501 /* translators: %s: new version available */
7502 __("Update to %s", "desktop-mode"),
7503 update.new_version
7504 );
7505 titleRow.appendChild(upd);
7506 }
7507 titleBlock.appendChild(titleRow);
7508 const byline = document.createElement("p");
7509 byline.className = "desktop-mode-plugins__detail-byline";
7510 const authorText = stripHtml$1(row.author ?? "") || __("Unknown author", "desktop-mode");
7511 if (row.author_uri) {
7512 const a = document.createElement("a");
7513 a.href = row.author_uri;
7514 a.target = "_blank";
7515 a.rel = "noopener noreferrer";
7516 a.textContent = authorText;
7517 a.setAttribute("data-noclick", "");
7518 byline.append(__("by", "desktop-mode") + " ", a);
7519 } else {
7520 byline.textContent = sprintf(
7521 /* translators: %s: plugin author */
7522 __("by %s", "desktop-mode"),
7523 authorText
7524 );
7525 }
7526 titleBlock.appendChild(byline);
7527 inner.append(iconTile, titleBlock);
7528 hero.appendChild(inner);
7529 return hero;
7530 }
7531 function renderTab(tab, row, slug, info) {
7532 if (tab === "overview") {
7533 return renderOverview(row, slug, info);
7534 }
7535 if (tab === "details") {
7536 return renderDetails(row);
7537 }
7538 if (tab === "changelog") {
7539 return renderChangelog(info);
7540 }
7541 if (tab === "faq") {
7542 return renderFaq(info);
7543 }
7544 return renderReviews(slug, info);
7545 }
7546 function renderOverview(row, slug, info) {
7547 const stack = document.createElement("wpd-stack");
7548 stack.setAttribute("gap", "20");
7549 const chipStrip = buildOverviewChips(row, info);
7550 if (chipStrip.children.length > 0) {
7551 stack.appendChild(chipStrip);
7552 }
7553 const descHtml = info?.sections?.description ?? info?.short_description ?? readDescription(row);
7554 if (descHtml) {
7555 const desc = document.createElement("div");
7556 desc.className = "desktop-mode-plugins__detail-html";
7557 desc.innerHTML = sanitizeHtml(descHtml);
7558 sanitizeLinks(desc);
7559 stack.appendChild(desc);
7560 } else if (slug && !info) {
7561 stack.appendChild(buildLoadingBlock(__("Loading description…", "desktop-mode")));
7562 } else {
7563 stack.appendChild(
7564 buildEmpty(
7565 "admin-plugins",
7566 __("No description", "desktop-mode"),
7567 __("This plugin doesn’t ship a description in its header.", "desktop-mode")
7568 )
7569 );
7570 }
7571 const actions = document.createElement("wpd-cluster");
7572 actions.setAttribute("gap", "8");
7573 actions.className = "desktop-mode-plugins__detail-actions";
7574 if (slug) {
7575 actions.appendChild(
7576 linkButton(
7577 "primary",
7578 __("View on WordPress.org", "desktop-mode"),
7579 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/`
7580 )
7581 );
7582 }
7583 if (row.plugin_uri) {
7584 actions.appendChild(
7585 linkButton("secondary", __("Plugin website", "desktop-mode"), row.plugin_uri)
7586 );
7587 }
7588 if (row.author_uri) {
7589 actions.appendChild(
7590 linkButton("ghost", __("Author website", "desktop-mode"), row.author_uri)
7591 );
7592 }
7593 if (actions.children.length > 0) {
7594 stack.appendChild(actions);
7595 }
7596 return stack;
7597 }
7598 function buildOverviewChips(row, info) {
7599 const strip = document.createElement("wpd-cluster");
7600 strip.setAttribute("gap", "8");
7601 strip.className = "desktop-mode-plugins__detail-chip-strip";
7602 if (info) {
7603 if (typeof info.rating === "number" && info.rating > 0) {
7604 const stars = document.createElement("span");
7605 stars.className = "desktop-mode-plugins__detail-stars-pill";
7606 stars.appendChild(buildStarCluster(info.rating, info.num_ratings ?? 0));
7607 strip.appendChild(stars);
7608 }
7609 if (info.active_installs) {
7610 strip.appendChild(
7611 chip(
7612 "admin-users",
7613 sprintf(
7614 /* translators: %s: comma-grouped active install count */
7615 __("%s+ active installs", "desktop-mode"),
7616 new Intl.NumberFormat().format(info.active_installs)
7617 )
7618 )
7619 );
7620 }
7621 if (info.last_updated) {
7622 strip.appendChild(
7623 chip(
7624 "update",
7625 sprintf(
7626 /* translators: %s: date the plugin was last updated */
7627 __("Updated %s", "desktop-mode"),
7628 humanDate(info.last_updated)
7629 )
7630 )
7631 );
7632 }
7633 if (info.tested) {
7634 strip.appendChild(
7635 chip(
7636 "wordpress-alt",
7637 sprintf(
7638 /* translators: %s: maximum tested WordPress version */
7639 __("Tested up to WP %s", "desktop-mode"),
7640 info.tested
7641 )
7642 )
7643 );
7644 }
7645 }
7646 if (row.requires_wp) {
7647 strip.appendChild(
7648 chip(
7649 "wordpress",
7650 sprintf(
7651 /* translators: %s: minimum WordPress version */
7652 __("Requires WP %s+", "desktop-mode"),
7653 row.requires_wp
7654 )
7655 )
7656 );
7657 }
7658 if (row.requires_php) {
7659 strip.appendChild(
7660 chip(
7661 "editor-code",
7662 sprintf(
7663 /* translators: %s: minimum PHP version */
7664 __("Requires PHP %s+", "desktop-mode"),
7665 row.requires_php
7666 )
7667 )
7668 );
7669 }
7670 if (row.network_only) {
7671 strip.appendChild(
7672 chip("networking", __("Network only", "desktop-mode"))
7673 );
7674 }
7675 return strip;
7676 }
7677 function renderDetails(row) {
7678 const grid = document.createElement("wpd-grid");
7679 grid.setAttribute("columns", "2");
7680 grid.setAttribute("gap", "12");
7681 grid.className = "desktop-mode-plugins__detail-grid";
7682 pushFactCard(grid, "media-document", __("Plugin file", "desktop-mode"), codeNode(row.plugin));
7683 if (row.version) {
7684 pushFactCard(grid, "tag", __("Version", "desktop-mode"), row.version);
7685 }
7686 if (row.desktop_mode_size_kb !== null && row.desktop_mode_size_kb !== void 0) {
7687 pushFactCard(
7688 grid,
7689 "database",
7690 __("Size on disk", "desktop-mode"),
7691 formatSize$1(row.desktop_mode_size_kb)
7692 );
7693 }
7694 if (row.requires_wp) {
7695 pushFactCard(
7696 grid,
7697 "wordpress-alt",
7698 __("Requires WordPress", "desktop-mode"),
7699 sprintf(
7700 /* translators: %s: version */
7701 __("%s+", "desktop-mode"),
7702 row.requires_wp
7703 )
7704 );
7705 }
7706 if (row.requires_php) {
7707 pushFactCard(
7708 grid,
7709 "editor-code",
7710 __("Requires PHP", "desktop-mode"),
7711 sprintf(
7712 /* translators: %s: version */
7713 __("%s+", "desktop-mode"),
7714 row.requires_php
7715 )
7716 );
7717 }
7718 if (row.textdomain) {
7719 pushFactCard(
7720 grid,
7721 "translation",
7722 __("Text domain", "desktop-mode"),
7723 codeNode(String(row.textdomain))
7724 );
7725 }
7726 if (row.plugin_uri) {
7727 pushFactCard(grid, "admin-links", __("Plugin URL", "desktop-mode"), externalLink(row.plugin_uri));
7728 }
7729 if (row.author_uri) {
7730 pushFactCard(grid, "admin-users", __("Author URL", "desktop-mode"), externalLink(row.author_uri));
7731 }
7732 if (row.network_only) {
7733 pushFactCard(
7734 grid,
7735 "networking",
7736 __("Scope", "desktop-mode"),
7737 __("Network only", "desktop-mode")
7738 );
7739 }
7740 pushFactCard(
7741 grid,
7742 row.status === "active" || row.status === "active-network" ? "yes-alt" : "marker",
7743 __("Status", "desktop-mode"),
7744 row.status === "active" || row.status === "active-network" ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode")
7745 );
7746 return grid;
7747 }
7748 function pushFactCard(parent, icon, label, value) {
7749 const card = document.createElement("wpd-card");
7750 card.setAttribute("compact", "");
7751 card.className = "desktop-mode-plugins__detail-fact";
7752 const head = document.createElement("div");
7753 head.setAttribute("slot", "header");
7754 head.className = "desktop-mode-plugins__detail-fact-head";
7755 const ico = document.createElement("span");
7756 ico.className = `dashicons dashicons-${icon}`;
7757 ico.setAttribute("aria-hidden", "true");
7758 const lab = document.createElement("span");
7759 lab.className = "desktop-mode-plugins__detail-fact-label";
7760 lab.textContent = label;
7761 head.append(ico, lab);
7762 card.appendChild(head);
7763 const val = document.createElement("div");
7764 val.className = "desktop-mode-plugins__detail-fact-value";
7765 if (typeof value === "string") {
7766 val.textContent = value;
7767 } else {
7768 val.appendChild(value);
7769 }
7770 card.appendChild(val);
7771 parent.appendChild(card);
7772 }
7773 function renderChangelog(info) {
7774 if (!info) {
7775 return buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode"));
7776 }
7777 const html2 = info.sections?.changelog;
7778 if (!html2) {
7779 return buildEmpty(
7780 "list-view",
7781 __("No changelog", "desktop-mode"),
7782 __("This plugin doesn’t ship a changelog.", "desktop-mode")
7783 );
7784 }
7785 const entries = parseChangelogEntries(html2);
7786 if (entries.length === 0) {
7787 const wrap = document.createElement("div");
7788 wrap.className = "desktop-mode-plugins__detail-html";
7789 wrap.innerHTML = sanitizeHtml(html2);
7790 sanitizeLinks(wrap);
7791 return wrap;
7792 }
7793 const stack = document.createElement("wpd-stack");
7794 stack.setAttribute("gap", "12");
7795 stack.className = "desktop-mode-plugins__detail-changelog";
7796 entries.forEach((entry, i) => {
7797 const card = document.createElement("wpd-card");
7798 card.className = "desktop-mode-plugins__detail-changelog-entry";
7799 const head = document.createElement("div");
7800 head.setAttribute("slot", "header");
7801 head.className = "desktop-mode-plugins__detail-changelog-head";
7802 const ver = document.createElement("wpd-badge");
7803 ver.setAttribute("tone", i === 0 ? "success" : "neutral");
7804 ver.textContent = entry.version;
7805 head.appendChild(ver);
7806 if (i === 0) {
7807 const latest = document.createElement("span");
7808 latest.className = "desktop-mode-plugins__detail-changelog-latest";
7809 latest.textContent = __("Latest", "desktop-mode");
7810 head.appendChild(latest);
7811 }
7812 card.appendChild(head);
7813 const body = document.createElement("div");
7814 body.className = "desktop-mode-plugins__detail-html";
7815 body.innerHTML = sanitizeHtml(entry.body);
7816 sanitizeLinks(body);
7817 card.appendChild(body);
7818 stack.appendChild(card);
7819 });
7820 return stack;
7821 }
7822 function parseChangelogEntries(html2) {
7823 const tmp = document.createElement("div");
7824 tmp.innerHTML = html2;
7825 if (tmp.childNodes.length === 0) {
7826 return [];
7827 }
7828 const entries = [];
7829 let current = null;
7830 const versionRegex = /([0-9]+\.[0-9]+(?:\.[0-9]+)?(?:[\w.+-]*)?)/;
7831 const flush = () => {
7832 if (!current) {
7833 return;
7834 }
7835 entries.push({ version: current.version, body: current.html.trim() });
7836 current = null;
7837 };
7838 for (const node of Array.from(tmp.childNodes)) {
7839 if (node.nodeType === Node.ELEMENT_NODE) {
7840 const el = node;
7841 const isHeading = /^H[1-6]$/.test(el.tagName);
7842 const text = (el.textContent ?? "").trim();
7843 const cleaned = text.replace(/^=+\s*|\s*=+$/g, "").trim();
7844 const headingMatch = isHeading ? cleaned.match(versionRegex) : null;
7845 if (headingMatch) {
7846 flush();
7847 current = { version: cleaned, html: "" };
7848 continue;
7849 }
7850 if (!current) {
7851 continue;
7852 }
7853 current.html += el.outerHTML;
7854 continue;
7855 }
7856 if (node.nodeType === Node.TEXT_NODE) {
7857 const text = node.textContent ?? "";
7858 if (!current) {
7859 continue;
7860 }
7861 if (text.trim() === "") {
7862 if (current.html !== "") {
7863 current.html += text;
7864 }
7865 continue;
7866 }
7867 current.html += `<p>${escapeHtml$1(text)}</p>`;
7868 }
7869 }
7870 flush();
7871 return entries;
7872 }
7873 function renderFaq(info) {
7874 if (!info) {
7875 return buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode"));
7876 }
7877 const html2 = info.sections?.faq;
7878 if (!html2) {
7879 return buildEmpty(
7880 "editor-help",
7881 __("No FAQ", "desktop-mode"),
7882 __("This plugin doesn’t ship an FAQ.", "desktop-mode")
7883 );
7884 }
7885 const pairs = parseFaqPairs(html2);
7886 if (pairs.length === 0) {
7887 const wrap = document.createElement("div");
7888 wrap.className = "desktop-mode-plugins__detail-html";
7889 wrap.innerHTML = sanitizeHtml(html2);
7890 sanitizeLinks(wrap);
7891 return wrap;
7892 }
7893 const stack = document.createElement("wpd-stack");
7894 stack.setAttribute("gap", "8");
7895 stack.className = "desktop-mode-plugins__detail-faq";
7896 pairs.forEach((pair, i) => {
7897 const item = document.createElement("details");
7898 item.className = "desktop-mode-plugins__detail-faq-item";
7899 if (i === 0) {
7900 item.setAttribute("open", "");
7901 }
7902 const summary = document.createElement("summary");
7903 summary.className = "desktop-mode-plugins__detail-faq-q";
7904 const qText = document.createElement("span");
7905 qText.className = "desktop-mode-plugins__detail-faq-q-text";
7906 qText.textContent = pair.question;
7907 const chevron = document.createElement("span");
7908 chevron.className = "desktop-mode-plugins__detail-faq-chevron";
7909 chevron.setAttribute("aria-hidden", "true");
7910 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>';
7911 summary.append(qText, chevron);
7912 const body = document.createElement("div");
7913 body.className = "desktop-mode-plugins__detail-faq-a desktop-mode-plugins__detail-html";
7914 body.innerHTML = sanitizeHtml(pair.answer);
7915 sanitizeLinks(body);
7916 item.append(summary, body);
7917 stack.appendChild(item);
7918 });
7919 return stack;
7920 }
7921 function parseFaqPairs(html2) {
7922 const tmp = document.createElement("div");
7923 tmp.innerHTML = html2;
7924 const dts = Array.from(tmp.querySelectorAll(":scope > dt"));
7925 if (dts.length > 0) {
7926 const pairs2 = [];
7927 for (const dt of dts) {
7928 pairs2.push(splitDtIntoPair(dt));
7929 }
7930 return pairs2.filter((p) => p.question !== "");
7931 }
7932 const dl = tmp.querySelector(":scope > dl");
7933 if (dl) {
7934 const pairs2 = [];
7935 let current2 = null;
7936 for (const node of Array.from(dl.children)) {
7937 if (node.tagName === "DT") {
7938 if (current2) {
7939 pairs2.push({ question: current2.q, answer: current2.html.trim() });
7940 }
7941 current2 = { q: (node.textContent ?? "").trim(), html: "" };
7942 } else if (node.tagName === "DD" && current2) {
7943 current2.html += node.innerHTML;
7944 } else if (current2) {
7945 current2.html += node.outerHTML;
7946 }
7947 }
7948 if (current2) {
7949 pairs2.push({ question: current2.q, answer: current2.html.trim() });
7950 }
7951 return pairs2.filter((p) => p.question !== "");
7952 }
7953 const pairs = [];
7954 let current = null;
7955 const flush = () => {
7956 if (!current) {
7957 return;
7958 }
7959 pairs.push({ question: current.q, answer: current.html.trim() });
7960 current = null;
7961 };
7962 for (const node of Array.from(tmp.childNodes)) {
7963 if (node.nodeType === Node.ELEMENT_NODE) {
7964 const el = node;
7965 const isHeading = /^H[1-6]$/.test(el.tagName);
7966 const text = (el.textContent ?? "").trim();
7967 if (isHeading && text) {
7968 flush();
7969 current = { q: text, html: "" };
7970 continue;
7971 }
7972 if (!current) {
7973 continue;
7974 }
7975 current.html += el.outerHTML;
7976 continue;
7977 }
7978 if (node.nodeType === Node.TEXT_NODE && current) {
7979 const text = node.textContent ?? "";
7980 if (text.trim() === "") {
7981 if (current.html !== "") {
7982 current.html += text;
7983 }
7984 continue;
7985 }
7986 current.html += `<p>${escapeHtml$1(text)}</p>`;
7987 }
7988 }
7989 flush();
7990 return pairs.filter((p) => p.question !== "");
7991 }
7992 function splitDtIntoPair(dt) {
7993 let question = "";
7994 let answerHtml = "";
7995 let seenElement = false;
7996 for (const child of Array.from(dt.childNodes)) {
7997 if (child.nodeType === Node.TEXT_NODE) {
7998 if (!seenElement) {
7999 question += child.textContent ?? "";
8000 } else {
8001 const txt = child.textContent ?? "";
8002 if (txt.trim() !== "") {
8003 answerHtml += `<p>${escapeHtml$1(txt)}</p>`;
8004 }
8005 }
8006 continue;
8007 }
8008 if (child.nodeType !== Node.ELEMENT_NODE) {
8009 continue;
8010 }
8011 const el = child;
8012 if (el.tagName === "P" && (el.textContent ?? "").trim() === "") {
8013 continue;
8014 }
8015 seenElement = true;
8016 answerHtml += el.outerHTML;
8017 }
8018 return {
8019 question: question.replace(/\s+/g, " ").trim(),
8020 answer: answerHtml.trim()
8021 };
8022 }
8023 function escapeHtml$1(text) {
8024 const tmp = document.createElement("span");
8025 tmp.textContent = text;
8026 return tmp.innerHTML;
8027 }
8028 function renderReviews(slug, info) {
8029 const stack = document.createElement("wpd-stack");
8030 stack.setAttribute("gap", "16");
8031 if (!info) {
8032 stack.appendChild(buildLoadingBlock(__("Loading from WordPress.org…", "desktop-mode")));
8033 return stack;
8034 }
8035 stack.appendChild(buildHistogram(info));
8036 const body = document.createElement("div");
8037 body.className = "desktop-mode-plugins__detail-reviews";
8038 stack.appendChild(body);
8039 const cached = reviewsCache.get(slug);
8040 if (cached) {
8041 paintReviewList(body, cached, slug);
8042 } else {
8043 body.appendChild(buildLoadingBlock(__("Loading recent reviews…", "desktop-mode")));
8044 void (async () => {
8045 try {
8046 const resp = await fetchPluginReviews(slug);
8047 reviewsCache.set(slug, resp);
8048 if (body.isConnected) {
8049 paintReviewList(body, resp, slug);
8050 }
8051 } catch {
8052 if (body.isConnected) {
8053 body.replaceChildren(
8054 buildEmpty(
8055 "warning",
8056 __("Couldn’t load reviews", "desktop-mode"),
8057 __("WordPress.org didn’t respond. Try again in a moment.", "desktop-mode")
8058 )
8059 );
8060 }
8061 }
8062 })();
8063 }
8064 return stack;
8065 }
8066 function paintReviewList(host, resp, slug) {
8067 host.replaceChildren();
8068 if (!resp.parsed) {
8069 host.appendChild(buildReviewsFallback(slug));
8070 return;
8071 }
8072 if (resp.items.length === 0) {
8073 host.appendChild(buildWriteReviewCta(slug));
8074 return;
8075 }
8076 const grid = document.createElement("wpd-grid");
8077 grid.setAttribute("columns", "2");
8078 grid.setAttribute("gap", "12");
8079 grid.className = "desktop-mode-plugins__detail-reviews-grid";
8080 for (const item of resp.items) {
8081 grid.appendChild(buildReviewCard(item));
8082 }
8083 host.appendChild(grid);
8084 const more = document.createElement("div");
8085 more.className = "desktop-mode-plugins__detail-reviews-more";
8086 more.appendChild(
8087 linkButton(
8088 "ghost",
8089 __("Read all reviews on WordPress.org ↗", "desktop-mode"),
8090 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/#reviews`
8091 )
8092 );
8093 host.appendChild(more);
8094 }
8095 function buildReviewsFallback(slug) {
8096 const empty = buildEmpty(
8097 "external",
8098 __("Reviews live on WordPress.org", "desktop-mode"),
8099 __(
8100 "We couldn’t pull the review feed here. Open the full thread on WordPress.org to read every review.",
8101 "desktop-mode"
8102 )
8103 );
8104 const cta = document.createElement("wpd-button");
8105 cta.setAttribute("slot", "cta");
8106 cta.setAttribute("variant", "primary");
8107 cta.setAttribute("size", "small");
8108 cta.setAttribute("data-noclick", "");
8109 cta.textContent = __("Open reviews on WordPress.org ↗", "desktop-mode");
8110 cta.addEventListener("click", () => {
8111 window.open(
8112 `https://wordpress.org/plugins/${encodeURIComponent(slug)}/#reviews`,
8113 "_blank",
8114 "noopener,noreferrer"
8115 );
8116 });
8117 empty.appendChild(cta);
8118 return empty;
8119 }
8120 function buildWriteReviewCta(slug) {
8121 const wrap = document.createElement("div");
8122 wrap.className = "desktop-mode-plugins__detail-reviews-cta";
8123 wrap.appendChild(
8124 linkButton(
8125 "primary",
8126 __("Write a review on WordPress.org ↗", "desktop-mode"),
8127 `https://wordpress.org/support/plugin/${encodeURIComponent(slug)}/reviews/#new-post`
8128 )
8129 );
8130 return wrap;
8131 }
8132 function buildReviewCard(item) {
8133 const card = document.createElement("wpd-card");
8134 card.setAttribute("compact", "");
8135 card.className = "desktop-mode-plugins__detail-review";
8136 const head = document.createElement("div");
8137 head.setAttribute("slot", "header");
8138 head.className = "desktop-mode-plugins__detail-review-head";
8139 const author = document.createElement("strong");
8140 author.textContent = item.author || __("Anonymous", "desktop-mode");
8141 head.appendChild(author);
8142 const stars = buildStarCluster(item.stars / 5 * 100, 0);
8143 head.appendChild(stars);
8144 if (item.date) {
8145 const date = document.createElement("span");
8146 date.className = "desktop-mode-plugins__detail-review-date";
8147 date.textContent = item.date;
8148 head.appendChild(date);
8149 }
8150 card.appendChild(head);
8151 if (item.excerpt) {
8152 const body = document.createElement("p");
8153 body.className = "desktop-mode-plugins__detail-review-body";
8154 body.textContent = item.excerpt;
8155 card.appendChild(body);
8156 }
8157 if (item.url) {
8158 const foot = document.createElement("div");
8159 foot.setAttribute("slot", "footer");
8160 const link = document.createElement("a");
8161 link.href = item.url;
8162 link.target = "_blank";
8163 link.rel = "noopener noreferrer";
8164 link.setAttribute("data-noclick", "");
8165 link.textContent = __("Read full review ↗", "desktop-mode");
8166 link.className = "desktop-mode-plugins__detail-review-link";
8167 foot.appendChild(link);
8168 card.appendChild(foot);
8169 }
8170 return card;
8171 }
8172 function buildHistogram(info) {
8173 const el = document.createElement("wpd-rating-summary");
8174 if (typeof info.rating === "number") {
8175 el.setAttribute("rating", String(info.rating));
8176 }
8177 if (info.num_ratings) {
8178 el.setAttribute("total", String(info.num_ratings));
8179 }
8180 const buckets = {};
8181 const ratings = info.ratings ?? {};
8182 for (const key of ["1", "2", "3", "4", "5"]) {
8183 const v = ratings[key];
8184 if (typeof v === "number") {
8185 buckets[key] = v;
8186 }
8187 }
8188 el.ratings = buckets;
8189 return el;
8190 }
8191 function chip(icon, label) {
8192 const c = document.createElement("wpd-chip");
8193 c.setAttribute("label", label);
8194 c.setAttribute("tone", "neutral");
8195 const ico = document.createElement("span");
8196 ico.setAttribute("slot", "icon");
8197 ico.className = `dashicons dashicons-${icon}`;
8198 ico.setAttribute("aria-hidden", "true");
8199 c.appendChild(ico);
8200 return c;
8201 }
8202 function buildLoadingBlock(label) {
8203 const wrap = document.createElement("div");
8204 wrap.className = "desktop-mode-plugins__detail-loading-block";
8205 const spinner = document.createElement("wpd-spinner");
8206 spinner.setAttribute("preset", "classic");
8207 spinner.setAttribute("size", "20");
8208 wrap.appendChild(spinner);
8209 const text = document.createElement("span");
8210 text.textContent = label;
8211 wrap.appendChild(text);
8212 return wrap;
8213 }
8214 function buildEmpty(icon, heading, description) {
8215 const e = document.createElement("wpd-empty-state");
8216 e.setAttribute("icon", `dashicons-${icon}`);
8217 e.setAttribute("heading", heading);
8218 e.setAttribute("description", description);
8219 return e;
8220 }
8221 function buildFallbackGlyph() {
8222 const span = document.createElement("span");
8223 span.className = "dashicons dashicons-admin-plugins";
8224 span.setAttribute("aria-hidden", "true");
8225 return span;
8226 }
8227 function linkButton(variant, label, href) {
8228 const btn = document.createElement("wpd-button");
8229 btn.setAttribute("variant", variant);
8230 btn.setAttribute("size", "small");
8231 btn.textContent = label;
8232 btn.setAttribute("data-noclick", "");
8233 btn.addEventListener("click", () => {
8234 window.open(href, "_blank", "noopener,noreferrer");
8235 });
8236 return btn;
8237 }
8238 function codeNode(text) {
8239 const code = document.createElement("code");
8240 code.textContent = text;
8241 return code;
8242 }
8243 function externalLink(href) {
8244 const a = document.createElement("a");
8245 a.href = href;
8246 a.target = "_blank";
8247 a.rel = "noopener noreferrer";
8248 a.textContent = href;
8249 a.setAttribute("data-noclick", "");
8250 return a;
8251 }
8252 function sanitizeLinks(wrap) {
8253 wrap.querySelectorAll("a").forEach((a) => {
8254 a.setAttribute("target", "_blank");
8255 a.setAttribute("rel", "noopener noreferrer");
8256 a.setAttribute("data-noclick", "");
8257 });
8258 }
8259 function deriveSlug(row) {
8260 if (!row.desktop_mode_icon_url) {
8261 return "";
8262 }
8263 const fromUpdate = row.desktop_mode_update_available?.slug;
8264 if (fromUpdate) {
8265 return fromUpdate;
8266 }
8267 const file = typeof row.plugin === "string" ? row.plugin : "";
8268 if (file) {
8269 const slash = file.indexOf("/");
8270 if (slash > 0) {
8271 return file.slice(0, slash);
8272 }
8273 }
8274 if (row.textdomain) {
8275 return String(row.textdomain);
8276 }
8277 return "";
8278 }
8279 function readDescription(row) {
8280 const d = row.description;
8281 if (!d) {
8282 return "";
8283 }
8284 if (typeof d === "string") {
8285 return d;
8286 }
8287 return d.rendered || d.raw || "";
8288 }
8289 function formatSize$1(kb) {
8290 if (kb < 1024) {
8291 return sprintf(
8292 /* translators: %d: kilobytes */
8293 __("%d KB", "desktop-mode"),
8294 kb
8295 );
8296 }
8297 return sprintf(
8298 /* translators: %s: megabytes (one decimal) */
8299 __("%s MB", "desktop-mode"),
8300 (kb / 1024).toFixed(1)
8301 );
8302 }
8303 function humanDate(raw) {
8304 const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raw);
8305 if (!m) {
8306 return raw;
8307 }
8308 try {
8309 return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])).toLocaleDateString();
8310 } catch {
8311 return raw;
8312 }
8313 }
8314 function stripHtml$1(html2) {
8315 const tmp = document.createElement("div");
8316 tmp.innerHTML = html2;
8317 return tmp.textContent ?? "";
8318 }
8319 function sanitizeHtml(html2) {
8320 const allowed = /* @__PURE__ */ new Set([
8321 "A",
8322 "ABBR",
8323 "B",
8324 "BLOCKQUOTE",
8325 "BR",
8326 "CODE",
8327 "DD",
8328 "DEL",
8329 "DIV",
8330 "DL",
8331 "DT",
8332 "EM",
8333 "FIGCAPTION",
8334 "FIGURE",
8335 "H1",
8336 "H2",
8337 "H3",
8338 "H4",
8339 "H5",
8340 "H6",
8341 "HR",
8342 "I",
8343 "IMG",
8344 "KBD",
8345 "LI",
8346 "OL",
8347 "P",
8348 "PRE",
8349 "Q",
8350 "S",
8351 "SMALL",
8352 "SPAN",
8353 "STRONG",
8354 "SUB",
8355 "SUP",
8356 "TABLE",
8357 "TBODY",
8358 "TD",
8359 "TFOOT",
8360 "TH",
8361 "THEAD",
8362 "TR",
8363 "U",
8364 "UL"
8365 ]);
8366 const allowedAttrs = /* @__PURE__ */ new Set([
8367 "href",
8368 "src",
8369 "alt",
8370 "title",
8371 "name",
8372 "rel",
8373 "target",
8374 "colspan",
8375 "rowspan"
8376 ]);
8377 const wrap = document.createElement("div");
8378 wrap.innerHTML = html2;
8379 const walker = document.createTreeWalker(wrap, NodeFilter.SHOW_ELEMENT);
8380 const toRemove = [];
8381 let current = walker.currentNode;
8382 while (current) {
8383 const next = walker.nextNode();
8384 if (current === wrap) {
8385 current = next;
8386 continue;
8387 }
8388 if (!allowed.has(current.tagName)) {
8389 toRemove.push(current);
8390 } else {
8391 for (const attr of Array.from(current.attributes)) {
8392 if (!allowedAttrs.has(attr.name.toLowerCase())) {
8393 current.removeAttribute(attr.name);
8394 }
8395 }
8396 if (current.tagName === "A") {
8397 const href = current.getAttribute("href") ?? "";
8398 if (href.toLowerCase().startsWith("javascript:")) {
8399 current.removeAttribute("href");
8400 }
8401 }
8402 if (current.tagName === "IMG") {
8403 const src = current.getAttribute("src") ?? "";
8404 if (src.toLowerCase().startsWith("javascript:")) {
8405 current.removeAttribute("src");
8406 }
8407 }
8408 }
8409 current = next;
8410 }
8411 for (const el of toRemove) {
8412 const text = document.createTextNode(el.textContent ?? "");
8413 el.replaceWith(text);
8414 }
8415 return wrap.innerHTML;
8416 }
8417 const PANEL_STYLES = `
8418 .desktop-mode-plugins__detail {
8419 display: block;
8420 background: var( --wpd-surface-subtle, rgba( 0, 0, 0, 0.025 ) );
8421 border-block-start: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8422 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8423 color: var( --wpd-fg, inherit );
8424 font-size: 13px;
8425 line-height: 1.55;
8426 }
8427
8428 /* Hero */
8429 .desktop-mode-plugins__detail-hero {
8430 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.6 ) );
8431 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8432 }
8433 .desktop-mode-plugins__detail-hero-inner {
8434 display: flex;
8435 align-items: center;
8436 gap: 14px;
8437 padding: 14px 24px;
8438 }
8439 .desktop-mode-plugins__detail-hero-icon {
8440 flex: 0 0 44px;
8441 width: 44px;
8442 height: 44px;
8443 border-radius: 10px;
8444 overflow: hidden;
8445 background: var( --wpd-surface, rgba( 0, 0, 0, 0.04 ) );
8446 box-shadow: 0 0 0 1px var( --wpd-border, rgba( 0, 0, 0, 0.08 ) ) inset;
8447 display: flex;
8448 align-items: center;
8449 justify-content: center;
8450 }
8451 .desktop-mode-plugins__detail-hero-icon img {
8452 width: 100%;
8453 height: 100%;
8454 max-width: 100%;
8455 max-height: 100%;
8456 object-fit: contain;
8457 display: block;
8458 }
8459 .desktop-mode-plugins__detail-hero-icon .dashicons {
8460 font-size: 20px;
8461 width: 20px;
8462 height: 20px;
8463 line-height: 20px;
8464 color: var( --wpd-fg-muted, #888 );
8465 }
8466 .desktop-mode-plugins__detail-hero-text {
8467 flex: 1 1 auto;
8468 min-width: 0;
8469 }
8470 .desktop-mode-plugins__detail-title {
8471 margin: 0;
8472 font-size: 15px;
8473 font-weight: 600;
8474 line-height: 1.25;
8475 letter-spacing: -0.005em;
8476 color: var( --wpd-fg, inherit );
8477 }
8478 .desktop-mode-plugins__detail-byline {
8479 margin: 0;
8480 font-size: 12.5px;
8481 color: var( --wpd-fg-muted, #666 );
8482 }
8483 .desktop-mode-plugins__detail-byline a {
8484 color: inherit;
8485 text-decoration: underline;
8486 text-decoration-color: var( --wpd-border-strong, rgba( 0, 0, 0, 0.25 ) );
8487 }
8488 .desktop-mode-plugins__detail-byline a:hover {
8489 color: var( --wp-admin-theme-color, #2271b1 );
8490 }
8491
8492 /* Tab strip */
8493 .desktop-mode-plugins__detail-tabs-wrap {
8494 padding: 0 24px;
8495 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.6 ) );
8496 border-block-end: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8497 }
8498 .desktop-mode-plugins__detail-tabs {
8499 display: block;
8500 }
8501
8502 /* Body */
8503 .desktop-mode-plugins__detail-body {
8504 padding: 22px 24px 26px;
8505 max-width: 100%;
8506 }
8507
8508 /* Overview chip strip */
8509 .desktop-mode-plugins__detail-chip-strip {
8510 display: flex;
8511 flex-wrap: wrap;
8512 gap: 8px;
8513 align-items: center;
8514 }
8515 .desktop-mode-plugins__detail-stars-pill {
8516 display: inline-flex;
8517 align-items: center;
8518 gap: 6px;
8519 padding: 4px 12px;
8520 border-radius: 999px;
8521 background: rgba( 234, 179, 8, 0.12 );
8522 color: #8a5a00;
8523 font-size: 12px;
8524 font-weight: 600;
8525 }
8526 .desktop-mode-plugins__detail-actions {
8527 padding-top: 4px;
8528 }
8529
8530 /* Sanitized HTML body (description / changelog / FAQ answers) */
8531 .desktop-mode-plugins__detail-html {
8532 color: var( --wpd-fg, inherit );
8533 font-size: 14px;
8534 line-height: 1.65;
8535 max-width: 78ch;
8536 }
8537 .desktop-mode-plugins__detail-html h1,
8538 .desktop-mode-plugins__detail-html h2,
8539 .desktop-mode-plugins__detail-html h3,
8540 .desktop-mode-plugins__detail-html h4 {
8541 margin: 16px 0 6px;
8542 line-height: 1.3;
8543 font-weight: 600;
8544 }
8545 .desktop-mode-plugins__detail-html h1 { font-size: 18px; }
8546 .desktop-mode-plugins__detail-html h2 { font-size: 16px; }
8547 .desktop-mode-plugins__detail-html h3 { font-size: 14.5px; }
8548 .desktop-mode-plugins__detail-html h4 { font-size: 13.5px; }
8549 .desktop-mode-plugins__detail-html p {
8550 margin: 0 0 10px;
8551 }
8552 .desktop-mode-plugins__detail-html ul,
8553 .desktop-mode-plugins__detail-html ol {
8554 margin: 0 0 10px;
8555 padding-inline-start: 22px;
8556 }
8557 .desktop-mode-plugins__detail-html li { margin-bottom: 4px; }
8558 .desktop-mode-plugins__detail-html code,
8559 .desktop-mode-plugins__detail-html pre {
8560 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8561 font-size: 12px;
8562 background: rgba( 0, 0, 0, 0.06 );
8563 border-radius: 4px;
8564 }
8565 .desktop-mode-plugins__detail-html code { padding: 1px 6px; }
8566 .desktop-mode-plugins__detail-html pre {
8567 padding: 10px 12px;
8568 overflow-x: auto;
8569 margin: 0 0 10px;
8570 }
8571 .desktop-mode-plugins__detail-html pre code {
8572 background: transparent;
8573 padding: 0;
8574 }
8575 .desktop-mode-plugins__detail-html a {
8576 color: var( --wp-admin-theme-color, #2271b1 );
8577 }
8578 .desktop-mode-plugins__detail-html img {
8579 display: block;
8580 max-width: 100%;
8581 max-height: 220px;
8582 width: auto;
8583 height: auto;
8584 object-fit: contain;
8585 margin: 8px 0;
8586 border-radius: 6px;
8587 }
8588
8589 /* Details fact cards */
8590 .desktop-mode-plugins__detail-grid {
8591 width: 100%;
8592 }
8593 .desktop-mode-plugins__detail-fact {
8594 min-width: 0;
8595 }
8596 .desktop-mode-plugins__detail-fact-head {
8597 display: flex;
8598 align-items: center;
8599 gap: 8px;
8600 color: var( --wpd-fg-muted, #666 );
8601 font-size: 11px;
8602 font-weight: 600;
8603 letter-spacing: 0.06em;
8604 text-transform: uppercase;
8605 }
8606 .desktop-mode-plugins__detail-fact-head .dashicons {
8607 font-size: 14px;
8608 width: 14px;
8609 height: 14px;
8610 line-height: 14px;
8611 }
8612 .desktop-mode-plugins__detail-fact-value {
8613 font-size: 14px;
8614 color: var( --wpd-fg, inherit );
8615 word-break: break-word;
8616 overflow-wrap: anywhere;
8617 font-weight: 500;
8618 }
8619 .desktop-mode-plugins__detail-fact-value code {
8620 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8621 font-size: 12.5px;
8622 background: rgba( 0, 0, 0, 0.06 );
8623 padding: 2px 7px;
8624 border-radius: 4px;
8625 font-weight: 400;
8626 }
8627 .desktop-mode-plugins__detail-fact-value a {
8628 color: var( --wp-admin-theme-color, #2271b1 );
8629 text-decoration: none;
8630 }
8631 .desktop-mode-plugins__detail-fact-value a:hover {
8632 text-decoration: underline;
8633 }
8634
8635 /* Changelog — version-grouped cards */
8636 .desktop-mode-plugins__detail-changelog {
8637 width: 100%;
8638 }
8639 .desktop-mode-plugins__detail-changelog-entry {
8640 width: 100%;
8641 }
8642 .desktop-mode-plugins__detail-changelog-head {
8643 display: flex;
8644 align-items: center;
8645 gap: 10px;
8646 }
8647 .desktop-mode-plugins__detail-changelog-latest {
8648 font-size: 11px;
8649 font-weight: 600;
8650 letter-spacing: 0.06em;
8651 text-transform: uppercase;
8652 color: var( --wpd-fg-muted, #666 );
8653 }
8654
8655 /* FAQ — accordion */
8656 .desktop-mode-plugins__detail-faq {
8657 width: 100%;
8658 }
8659 .desktop-mode-plugins__detail-faq-item {
8660 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.7 ) );
8661 border: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8662 border-radius: 12px;
8663 overflow: hidden;
8664 transition: box-shadow 160ms ease, border-color 160ms ease;
8665 }
8666 .desktop-mode-plugins__detail-faq-item[open] {
8667 border-color: var( --wp-admin-theme-color, #2271b1 );
8668 box-shadow: 0 4px 14px rgba( 0, 0, 0, 0.06 );
8669 }
8670 .desktop-mode-plugins__detail-faq-q {
8671 display: flex;
8672 align-items: center;
8673 gap: 10px;
8674 padding: 14px 16px;
8675 cursor: pointer;
8676 list-style: none;
8677 user-select: none;
8678 }
8679 .desktop-mode-plugins__detail-faq-q::-webkit-details-marker {
8680 display: none;
8681 }
8682 .desktop-mode-plugins__detail-faq-q:hover {
8683 background: rgba( 0, 0, 0, 0.025 );
8684 }
8685 .desktop-mode-plugins__detail-faq-q-text {
8686 flex: 1 1 auto;
8687 font-size: 14px;
8688 font-weight: 600;
8689 color: var( --wpd-fg, inherit );
8690 line-height: 1.4;
8691 }
8692 .desktop-mode-plugins__detail-faq-chevron {
8693 flex: 0 0 auto;
8694 width: 24px;
8695 height: 24px;
8696 border-radius: 50%;
8697 display: inline-flex;
8698 align-items: center;
8699 justify-content: center;
8700 background: rgba( 0, 0, 0, 0.05 );
8701 color: var( --wpd-fg-muted, #555 );
8702 transition: transform 200ms cubic-bezier( 0.2, 0.8, 0.2, 1 ), background 160ms ease;
8703 }
8704 .desktop-mode-plugins__detail-faq-item[open] .desktop-mode-plugins__detail-faq-chevron {
8705 transform: rotate( 180deg );
8706 background: var( --wp-admin-theme-color, #2271b1 );
8707 color: #fff;
8708 }
8709 .desktop-mode-plugins__detail-faq-a {
8710 padding: 4px 16px 16px;
8711 border-block-start: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.06 ) );
8712 background: rgba( 0, 0, 0, 0.012 );
8713 }
8714 @media ( prefers-reduced-motion: reduce ) {
8715 .desktop-mode-plugins__detail-faq-chevron,
8716 .desktop-mode-plugins__detail-faq-item {
8717 transition: none;
8718 }
8719 }
8720
8721 /* Reviews */
8722 .desktop-mode-plugins__detail-reviews {
8723 width: 100%;
8724 }
8725 .desktop-mode-plugins__detail-reviews-grid {
8726 width: 100%;
8727 }
8728 .desktop-mode-plugins__detail-reviews-more,
8729 .desktop-mode-plugins__detail-reviews-cta {
8730 display: flex;
8731 justify-content: center;
8732 padding-top: 12px;
8733 }
8734 .desktop-mode-plugins__detail-review {
8735 width: 100%;
8736 height: 100%;
8737 box-sizing: border-box;
8738 }
8739 .desktop-mode-plugins__detail-review-body {
8740 display: -webkit-box;
8741 -webkit-line-clamp: 4;
8742 -webkit-box-orient: vertical;
8743 overflow: hidden;
8744 }
8745 @media ( max-width: 720px ) {
8746 .desktop-mode-plugins__detail-reviews-grid {
8747 grid-template-columns: 1fr !important;
8748 }
8749 }
8750 .desktop-mode-plugins__detail-review-head {
8751 display: flex;
8752 align-items: center;
8753 gap: 10px;
8754 flex-wrap: wrap;
8755 }
8756 .desktop-mode-plugins__detail-review-date {
8757 margin-inline-start: auto;
8758 font-size: 11.5px;
8759 color: var( --wpd-fg-muted, #888 );
8760 }
8761 .desktop-mode-plugins__detail-review-body {
8762 margin: 0;
8763 font-size: 13px;
8764 color: var( --wpd-fg, inherit );
8765 line-height: 1.55;
8766 }
8767 .desktop-mode-plugins__detail-review-link {
8768 font-size: 12px;
8769 font-weight: 600;
8770 color: var( --wp-admin-theme-color, #2271b1 );
8771 text-decoration: none;
8772 }
8773 .desktop-mode-plugins__detail-review-link:hover {
8774 text-decoration: underline;
8775 }
8776
8777 /* Loading block */
8778 .desktop-mode-plugins__detail-loading-block {
8779 display: inline-flex;
8780 align-items: center;
8781 gap: 10px;
8782 padding: 12px 14px;
8783 border-radius: 10px;
8784 background: var( --wpd-surface-raised, rgba( 255, 255, 255, 0.7 ) );
8785 border: 1px solid var( --wpd-border, rgba( 0, 0, 0, 0.08 ) );
8786 color: var( --wpd-fg-muted, #666 );
8787 font-size: 13px;
8788 }
8789
8790 @media ( max-width: 720px ) {
8791 .desktop-mode-plugins__detail-grid {
8792 grid-template-columns: 1fr !important;
8793 }
8794 }
8795 `;
8796 const PLUGINS_CHANGED_TOPIC = "desktop-mode.plugin.changed";
8797 const SOURCE = "installed-view";
8798 function toast(message, duration = 3500) {
8799 const api2 = window.wp?.desktop;
8800 if (api2 && typeof api2.showToast === "function") {
8801 api2.showToast({ message, duration });
8802 return;
8803 }
8804 console.log("[plugins-window]", message);
8805 }
8806 async function confirm(opts) {
8807 const api2 = window.wp?.desktop;
8808 if (api2 && typeof api2.confirm === "function") {
8809 return api2.confirm(opts);
8810 }
8811 return Promise.resolve(true);
8812 }
8813 function mountInstalledView(host) {
8814 host.replaceChildren();
8815 const state = {
8816 rows: [],
8817 statusFilter: "",
8818 search: "",
8819 loading: true,
8820 updating: /* @__PURE__ */ new Set(),
8821 autoUpdating: /* @__PURE__ */ new Set()
8822 };
8823 const toolbar = document.createElement("header");
8824 toolbar.className = "desktop-mode-plugins__toolbar";
8825 const left = document.createElement("div");
8826 left.className = "desktop-mode-plugins__toolbar-left";
8827 const statusFilter = document.createElement("wpd-segmented");
8828 statusFilter.setAttribute("value", "");
8829 const statusOptions = [
8830 { value: "", label: __("All", "desktop-mode") },
8831 { value: "active", label: __("Active", "desktop-mode") },
8832 { value: "inactive", label: __("Inactive", "desktop-mode") },
8833 { value: "update", label: __("Update available", "desktop-mode") }
8834 ];
8835 let updateCountBadge = null;
8836 for (const opt of statusOptions) {
8837 const seg = document.createElement("wpd-segment");
8838 seg.setAttribute("value", opt.value);
8839 if (opt.value === "update") {
8840 const label = document.createElement("span");
8841 label.textContent = opt.label;
8842 seg.appendChild(label);
8843 const badge = document.createElement("wpd-badge");
8844 badge.setAttribute("tone", "warning");
8845 badge.setAttribute("no-dot", "");
8846 badge.style.cssText = "margin-inline-start:6px;";
8847 badge.hidden = true;
8848 seg.appendChild(badge);
8849 updateCountBadge = badge;
8850 } else {
8851 seg.textContent = opt.label;
8852 }
8853 statusFilter.appendChild(seg);
8854 }
8855 statusFilter.addEventListener("wpd-pick", (ev) => {
8856 const detail = ev.detail;
8857 state.statusFilter = detail?.value ?? "";
8858 paintTable();
8859 });
8860 const search = document.createElement("wpd-text-field");
8861 search.setAttribute(
8862 "placeholder",
8863 __("Search installed plugins…", "desktop-mode")
8864 );
8865 let searchDebounce;
8866 search.addEventListener("wpd-input-change", (ev) => {
8867 const value = ev.detail?.value ?? "";
8868 window.clearTimeout(searchDebounce);
8869 searchDebounce = window.setTimeout(() => {
8870 state.search = value;
8871 paintTable();
8872 }, 200);
8873 });
8874 left.append(statusFilter, search);
8875 const right = document.createElement("div");
8876 right.className = "desktop-mode-plugins__toolbar-right";
8877 const bulkBar = document.createElement("div");
8878 bulkBar.className = "desktop-mode-plugins__bulk";
8879 bulkBar.hidden = true;
8880 right.appendChild(bulkBar);
8881 const trailing = document.createElement("div");
8882 trailing.className = "desktop-mode-plugins__toolbar-trailing";
8883 const refreshButton = document.createElement("wpd-button");
8884 refreshButton.setAttribute("variant", "ghost");
8885 refreshButton.setAttribute("title", __("Refresh", "desktop-mode"));
8886 refreshButton.innerHTML = '<span class="dashicons dashicons-update" aria-hidden="true"></span>';
8887 refreshButton.addEventListener("click", () => {
8888 void (async () => {
8889 await reload({ force: true });
8890 void refreshFrameworkMenu();
8891 })();
8892 });
8893 trailing.appendChild(refreshButton);
8894 toolbar.append(left, right, trailing);
8895 const tableWrap = document.createElement("div");
8896 tableWrap.className = "desktop-mode-plugins__body";
8897 const table = document.createElement("wpd-table");
8898 table.setAttribute("selectable", "multi");
8899 table.setAttribute("sticky-header", "");
8900 table.setAttribute("sticky-columns", "1");
8901 table.setAttribute("hover", "");
8902 table.setAttribute("striped", "");
8903 table.setAttribute("bordered", "");
8904 table.setAttribute("loading", "");
8905 table.setAttribute("data-installed-rows", "");
8906 const empty = document.createElement("div");
8907 empty.setAttribute("slot", "empty");
8908 empty.className = "desktop-mode-plugins__empty";
8909 empty.innerHTML = '<span class="dashicons dashicons-admin-plugins" aria-hidden="true"></span><p>' + __("No plugins match your filters.", "desktop-mode") + "</p>";
8910 table.appendChild(empty);
8911 const getRowId = (row, index) => row.plugin || String(index);
8912 table.getRowId = getRowId;
8913 table.columns = buildColumns();
8914 table.subTable = (row) => buildInstalledDetail(row);
8915 table.addEventListener("wpd-table-row-click", (ev) => {
8916 const detail = ev.detail;
8917 if (!detail) {
8918 return;
8919 }
8920 if (table.isExpanded(detail.index)) {
8921 table.collapse(detail.index);
8922 } else {
8923 table.expand(detail.index);
8924 }
8925 });
8926 tableWrap.appendChild(table);
8927 host.append(toolbar, tableWrap);
8928 const selectionListener = (ev) => {
8929 const detail = ev.detail;
8930 const ids = detail?.selection ?? [];
8931 paintBulkBar(ids);
8932 };
8933 table.addEventListener("wpd-table-selection-change", selectionListener);
8934 void reload();
8935 function buildColumns() {
8936 const cfg = getConfig();
8937 const cols = [
8938 {
8939 key: "name",
8940 label: __("Plugin", "desktop-mode"),
8941 sortable: true,
8942 sticky: true,
8943 render: (_value, row) => renderNameCell(row)
8944 },
8945 {
8946 key: "status",
8947 label: __("Status", "desktop-mode"),
8948 sortable: true,
8949 render: (_value, row) => renderStatusCell(row)
8950 },
8951 {
8952 key: "version",
8953 label: __("Version", "desktop-mode"),
8954 sortable: true,
8955 render: (_value, row) => renderVersionCell(row)
8956 },
8957 {
8958 key: "author",
8959 label: __("Author", "desktop-mode"),
8960 render: (_value, row) => renderAuthorCell(row)
8961 },
8962 {
8963 key: "desktop_mode_size_kb",
8964 label: __("Size", "desktop-mode"),
8965 align: "end",
8966 sortable: true,
8967 sortValue: (row) => row.desktop_mode_size_kb ?? 0,
8968 render: (_value, row) => formatSize(row.desktop_mode_size_kb ?? null)
8969 }
8970 ];
8971 if (cfg.autoUpdatesEnabled) {
8972 cols.push({
8973 key: "auto_updates",
8974 label: __("Automatic Updates", "desktop-mode"),
8975 sortable: true,
8976 sortValue: (row) => row.desktop_mode_auto_update?.enabled ? 1 : 0,
8977 render: (_value, row) => renderAutoUpdateCell(row)
8978 });
8979 }
8980 cols.push({
8981 key: "_actions",
8982 label: "",
8983 align: "end",
8984 render: (_value, row) => renderActionsCell(row)
8985 });
8986 return cfg.caps.activate || cfg.caps.delete ? cols : cols.slice(0, -1);
8987 }
8988 function renderNameCell(row) {
8989 const wrap = document.createElement("div");
8990 wrap.style.cssText = "display:flex;align-items:center;gap:12px;min-width:0;padding:4px 0;";
8991 const icon = document.createElement("div");
8992 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;";
8993 const url = row.desktop_mode_icon_url;
8994 if (url) {
8995 const img = document.createElement("img");
8996 img.alt = "";
8997 img.loading = "lazy";
8998 img.decoding = "async";
8999 img.style.cssText = "width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;display:block;";
9000 img.src = attachIconFallback(img, url, () => {
9001 icon.replaceChildren(buildFallbackIcon());
9002 });
9003 icon.appendChild(img);
9004 } else {
9005 icon.appendChild(buildFallbackIcon());
9006 }
9007 const text = document.createElement("div");
9008 text.style.cssText = "display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto;line-height:1.35;";
9009 const title = document.createElement("strong");
9010 title.textContent = row.name || row.plugin;
9011 title.style.cssText = "display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
9012 const path = document.createElement("span");
9013 path.textContent = row.plugin;
9014 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;";
9015 text.append(title, path);
9016 wrap.append(icon, text);
9017 return wrap;
9018 }
9019 function buildFallbackIcon() {
9020 const fallback = document.createElement("span");
9021 fallback.className = "dashicons dashicons-admin-plugins";
9022 fallback.setAttribute("aria-hidden", "true");
9023 fallback.style.cssText = "font-size:18px;width:18px;height:18px;line-height:18px;color:#888;";
9024 return fallback;
9025 }
9026 function renderStatusCell(row) {
9027 const badge = document.createElement("span");
9028 const isActive = row.status === "active" || row.status === "active-network";
9029 const dot = isActive ? "#16a34a" : "#9ca3af";
9030 const bg = isActive ? "rgba(22, 163, 74, 0.14)" : "rgba(120, 120, 120, 0.12)";
9031 const fg = isActive ? "#166e37" : "#555";
9032 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};`;
9033 const dotEl = document.createElement("span");
9034 dotEl.style.cssText = `width:6px;height:6px;border-radius:50%;background:${dot};flex:0 0 auto;display:inline-block;`;
9035 badge.appendChild(dotEl);
9036 const label = document.createElement("span");
9037 label.textContent = isActive ? __("Active", "desktop-mode") : __("Inactive", "desktop-mode");
9038 badge.appendChild(label);
9039 return badge;
9040 }
9041 function renderVersionCell(row) {
9042 const wrap = document.createElement("div");
9043 wrap.style.cssText = "display:flex;align-items:center;gap:6px;flex-wrap:wrap;";
9044 const v = document.createElement("span");
9045 v.textContent = row.version ?? "";
9046 wrap.appendChild(v);
9047 const update = row.desktop_mode_update_available;
9048 if (update?.available && update.new_version) {
9049 const badge = document.createElement("span");
9050 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;";
9051 badge.textContent = sprintf(
9052 /* translators: %s: new plugin version */
9053 __("→ %s", "desktop-mode"),
9054 update.new_version
9055 );
9056 wrap.appendChild(badge);
9057 }
9058 return wrap;
9059 }
9060 function renderAuthorCell(row) {
9061 const wrap = document.createElement("span");
9062 wrap.style.cssText = "white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;";
9063 const text = stripHtml(row.author ?? "");
9064 wrap.textContent = text || __("Unknown", "desktop-mode");
9065 return wrap;
9066 }
9067 function renderAutoUpdateCell(row) {
9068 const wrap = document.createElement("div");
9069 wrap.setAttribute("data-noclick", "");
9070 wrap.style.cssText = "display:inline-flex;align-items:center;gap:6px;white-space:nowrap;";
9071 const meta = row.desktop_mode_auto_update;
9072 const forced = meta?.forced ?? null;
9073 if (forced !== null) {
9074 const label2 = document.createElement("span");
9075 label2.style.cssText = "color:var(--wp-desktop-text-muted,#666);";
9076 label2.textContent = forced ? __("Auto-updates enabled", "desktop-mode") : __("Auto-updates disabled", "desktop-mode");
9077 wrap.appendChild(label2);
9078 return wrap;
9079 }
9080 const supported = !!meta?.supported;
9081 if (!supported) {
9082 const placeholder = document.createElement("span");
9083 placeholder.style.cssText = "color:var(--wp-desktop-text-muted,#9ca3af);";
9084 placeholder.textContent = "";
9085 placeholder.title = __(
9086 "This plugin does not check in with WordPress.org, so automatic updates can't be scheduled.",
9087 "desktop-mode"
9088 );
9089 wrap.appendChild(placeholder);
9090 return wrap;
9091 }
9092 const enabled = !!meta?.enabled;
9093 const busy = state.autoUpdating.has(row.plugin);
9094 const link = document.createElement("a");
9095 link.href = "#";
9096 link.setAttribute("role", "button");
9097 link.setAttribute("data-wp-action", enabled ? "disable" : "enable");
9098 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;";
9099 if (busy) {
9100 link.style.opacity = "0.6";
9101 link.style.pointerEvents = "none";
9102 link.setAttribute("aria-busy", "true");
9103 }
9104 const label = document.createElement("span");
9105 if (busy) {
9106 label.textContent = enabled ? __("Disabling…", "desktop-mode") : __("Enabling…", "desktop-mode");
9107 } else {
9108 label.textContent = enabled ? __("Disable auto-updates", "desktop-mode") : __("Enable auto-updates", "desktop-mode");
9109 }
9110 link.appendChild(label);
9111 link.addEventListener("click", (e) => {
9112 e.preventDefault();
9113 e.stopPropagation();
9114 void runToggleAutoUpdate(row);
9115 });
9116 wrap.appendChild(link);
9117 return wrap;
9118 }
9119 function renderActionsCell(row) {
9120 const wrap = document.createElement("div");
9121 wrap.style.cssText = "display:inline-flex;gap:8px;align-items:center;justify-content:flex-end;flex-wrap:nowrap;";
9122 wrap.setAttribute("data-noclick", "");
9123 const can = row.desktop_mode_can_manage ?? {
9124 activate: row.status === "inactive",
9125 deactivate: row.status === "active" || row.status === "active-network",
9126 delete: row.status === "inactive"
9127 };
9128 const update = row.desktop_mode_update_available;
9129 if (getConfig().caps.update && update?.available) {
9130 if (update.package) {
9131 const updating = state.updating.has(row.plugin);
9132 const label = updating ? __("Updating…", "desktop-mode") : sprintf(
9133 /* translators: %s: new plugin version (e.g. "1.4.2") */
9134 __("Update to %s", "desktop-mode"),
9135 update.new_version ?? ""
9136 );
9137 const btn = button2(label, "primary");
9138 if (updating) {
9139 btn.setAttribute("disabled", "");
9140 btn.setAttribute("aria-busy", "true");
9141 }
9142 btn.addEventListener("click", (e) => {
9143 e.stopPropagation();
9144 void runUpdate(row);
9145 });
9146 wrap.appendChild(btn);
9147 } else {
9148 const hint = document.createElement("span");
9149 hint.style.cssText = "font-size:0.78em;color:var(--wp-desktop-text-muted,#666);";
9150 hint.textContent = __("Auto-update unavailable", "desktop-mode");
9151 hint.title = __(
9152 "This plugin does not ship a wp.org download package. Update it manually from its source.",
9153 "desktop-mode"
9154 );
9155 wrap.appendChild(hint);
9156 }
9157 }
9158 if (can.activate) {
9159 const btn = button2(__("Activate", "desktop-mode"), "primary");
9160 btn.addEventListener("click", (e) => {
9161 e.stopPropagation();
9162 void runActivate(row);
9163 });
9164 wrap.appendChild(btn);
9165 } else if (can.deactivate) {
9166 const btn = button2(__("Deactivate", "desktop-mode"), "secondary");
9167 btn.addEventListener("click", (e) => {
9168 e.stopPropagation();
9169 void runDeactivate(row);
9170 });
9171 wrap.appendChild(btn);
9172 }
9173 if (can.delete) {
9174 const btn = button2(__("Delete", "desktop-mode"), "danger");
9175 btn.addEventListener("click", (e) => {
9176 e.stopPropagation();
9177 void runDelete(row);
9178 });
9179 wrap.appendChild(btn);
9180 }
9181 return wrap;
9182 }
9183 function button2(label, variant) {
9184 const b = document.createElement("wpd-button");
9185 b.setAttribute("variant", variant);
9186 b.setAttribute("size", "small");
9187 b.textContent = label;
9188 return b;
9189 }
9190 function paintBulkBar(ids) {
9191 bulkBar.replaceChildren();
9192 if (ids.length === 0) {
9193 bulkBar.hidden = true;
9194 return;
9195 }
9196 bulkBar.hidden = false;
9197 const count = document.createElement("span");
9198 count.className = "desktop-mode-plugins__bulk-count";
9199 count.textContent = sprintf(
9200 /* translators: %d: number of selected plugins */
9201 __("%d selected", "desktop-mode"),
9202 ids.length
9203 );
9204 bulkBar.appendChild(count);
9205 const cfg = getConfig();
9206 const selected = state.rows.filter((r) => ids.includes(r.plugin));
9207 if (cfg.caps.update) {
9208 const updatable = selected.filter(
9209 (r) => !!r.desktop_mode_update_available?.available && !!r.desktop_mode_update_available.package
9210 );
9211 if (updatable.length > 0) {
9212 const btn = button2(
9213 sprintf(
9214 /* translators: %d: number of plugins with pending updates */
9215 __("Update %d", "desktop-mode"),
9216 updatable.length
9217 ),
9218 "primary"
9219 );
9220 btn.addEventListener("click", () => {
9221 void runBulk(updatable, "update");
9222 });
9223 bulkBar.appendChild(btn);
9224 }
9225 }
9226 if (cfg.caps.activate) {
9227 const activatable = selected.filter((r) => r.status === "inactive");
9228 if (activatable.length > 0) {
9229 const btn = button2(__("Activate", "desktop-mode"), "primary");
9230 btn.addEventListener("click", () => {
9231 void runBulk(activatable, "activate");
9232 });
9233 bulkBar.appendChild(btn);
9234 }
9235 const deactivatable = selected.filter(
9236 (r) => r.status === "active" || r.status === "active-network"
9237 );
9238 if (deactivatable.length > 0) {
9239 const btn = button2(__("Deactivate", "desktop-mode"), "secondary");
9240 btn.addEventListener("click", () => {
9241 void runBulk(deactivatable, "deactivate");
9242 });
9243 bulkBar.appendChild(btn);
9244 }
9245 }
9246 if (cfg.caps.delete) {
9247 const deletable = selected.filter((r) => r.status === "inactive");
9248 if (deletable.length > 0) {
9249 const btn = button2(__("Delete", "desktop-mode"), "danger");
9250 btn.addEventListener("click", () => {
9251 void runBulk(deletable, "delete");
9252 });
9253 bulkBar.appendChild(btn);
9254 }
9255 }
9256 }
9257 async function reload(opts = {}) {
9258 state.loading = true;
9259 table.setAttribute("loading", "");
9260 try {
9261 state.rows = await fetchInstalledPlugins(opts);
9262 } catch (err) {
9263 toast(
9264 sprintf(
9265 /* translators: %s: error message */
9266 __("Could not load plugins: %s", "desktop-mode"),
9267 describe(err)
9268 ),
9269 6e3
9270 );
9271 state.rows = [];
9272 }
9273 state.loading = false;
9274 paintTable();
9275 }
9276 function paintTable() {
9277 if (state.loading) {
9278 table.setAttribute("loading", "");
9279 } else {
9280 table.removeAttribute("loading");
9281 }
9282 table.data = filterRows(state.rows);
9283 paintUpdateCount();
9284 }
9285 function paintUpdateCount() {
9286 if (!updateCountBadge) {
9287 return;
9288 }
9289 const count = state.rows.filter(
9290 (r) => !!r.desktop_mode_update_available?.available
9291 ).length;
9292 if (count > 0) {
9293 updateCountBadge.textContent = String(count);
9294 updateCountBadge.hidden = false;
9295 } else {
9296 updateCountBadge.hidden = true;
9297 updateCountBadge.textContent = "";
9298 }
9299 }
9300 function filterRows(rows) {
9301 const q = state.search.trim().toLowerCase();
9302 const status = state.statusFilter;
9303 return rows.filter((row) => {
9304 if (status === "active") {
9305 if (row.status !== "active" && row.status !== "active-network") {
9306 return false;
9307 }
9308 } else if (status === "inactive") {
9309 if (row.status !== "inactive") {
9310 return false;
9311 }
9312 } else if (status === "update") {
9313 if (!row.desktop_mode_update_available?.available) {
9314 return false;
9315 }
9316 }
9317 if (q !== "") {
9318 const haystack = `${row.name ?? ""} ${row.plugin} ${stripHtml(row.author ?? "")}`.toLowerCase();
9319 if (!haystack.includes(q)) {
9320 return false;
9321 }
9322 }
9323 return true;
9324 });
9325 }
9326 async function runActivate(row) {
9327 const previous = row.status;
9328 applyStatusOptimistic(row, "active");
9329 try {
9330 const updated = await activateInstalledPlugin(row);
9331 mergeRow(updated);
9332 toast(
9333 sprintf(
9334 /* translators: %s: plugin name */
9335 __("%s activated.", "desktop-mode"),
9336 row.name || row.plugin
9337 )
9338 );
9339 broadcast(PLUGINS_CHANGED_TOPIC, {
9340 source: SOURCE,
9341 plugin: row.plugin,
9342 action: "activate"
9343 });
9344 void refreshFrameworkMenu();
9345 } catch (err) {
9346 applyStatusOptimistic(row, previous);
9347 toast(
9348 sprintf(
9349 /* translators: %s: error message */
9350 __("Activation failed: %s", "desktop-mode"),
9351 describe(err)
9352 ),
9353 6e3
9354 );
9355 }
9356 }
9357 async function runDeactivate(row) {
9358 const previous = row.status;
9359 applyStatusOptimistic(row, "inactive");
9360 try {
9361 const updated = await deactivateInstalledPlugin(row);
9362 mergeRow(updated);
9363 if (isDesktopModeSelf(row.plugin)) {
9364 toast(
9365 __(
9366 "Desktop Mode deactivated. Reloading…",
9367 "desktop-mode"
9368 ),
9369 2e3
9370 );
9371 reloadOutOfDesktopMode();
9372 return;
9373 }
9374 toast(
9375 sprintf(
9376 /* translators: %s: plugin name */
9377 __("%s deactivated.", "desktop-mode"),
9378 row.name || row.plugin
9379 )
9380 );
9381 broadcast(PLUGINS_CHANGED_TOPIC, {
9382 source: SOURCE,
9383 plugin: row.plugin,
9384 action: "deactivate"
9385 });
9386 void refreshFrameworkMenu();
9387 } catch (err) {
9388 applyStatusOptimistic(row, previous);
9389 toast(
9390 sprintf(
9391 /* translators: %s: error message */
9392 __("Deactivation failed: %s", "desktop-mode"),
9393 describe(err)
9394 ),
9395 6e3
9396 );
9397 }
9398 }
9399 async function runDelete(row) {
9400 const ok = await confirm({
9401 title: __("Delete plugin?", "desktop-mode"),
9402 message: sprintf(
9403 /* translators: %s: plugin name */
9404 __(
9405 "Permanently delete %s? Its files will be removed from disk. This cannot be undone.",
9406 "desktop-mode"
9407 ),
9408 row.name || row.plugin
9409 ),
9410 confirmLabel: __("Delete", "desktop-mode"),
9411 danger: true
9412 });
9413 if (!ok) {
9414 return;
9415 }
9416 try {
9417 await deleteInstalledPlugin(row);
9418 state.rows = state.rows.filter((r) => r.plugin !== row.plugin);
9419 paintTable();
9420 if (isDesktopModeSelf(row.plugin)) {
9421 toast(
9422 __(
9423 "Desktop Mode deleted. Reloading…",
9424 "desktop-mode"
9425 ),
9426 2e3
9427 );
9428 reloadOutOfDesktopMode();
9429 return;
9430 }
9431 toast(
9432 sprintf(
9433 /* translators: %s: plugin name */
9434 __("%s deleted.", "desktop-mode"),
9435 row.name || row.plugin
9436 )
9437 );
9438 broadcast(PLUGINS_CHANGED_TOPIC, {
9439 source: SOURCE,
9440 plugin: row.plugin,
9441 action: "delete"
9442 });
9443 void refreshFrameworkMenu();
9444 } catch (err) {
9445 toast(
9446 sprintf(
9447 /* translators: %s: error message */
9448 __("Delete failed: %s", "desktop-mode"),
9449 describe(err)
9450 ),
9451 6e3
9452 );
9453 }
9454 }
9455 async function runUpdate(row) {
9456 if (state.updating.has(row.plugin)) {
9457 return;
9458 }
9459 state.updating.add(row.plugin);
9460 paintTable();
9461 try {
9462 const result = await enqueueUpdateJob(() => updateInstalledPlugin(row));
9463 mergeRow({
9464 ...row,
9465 version: result.newVersion,
9466 desktop_mode_update_available: {
9467 available: false,
9468 new_version: null,
9469 package: "",
9470 slug: row.desktop_mode_update_available?.slug ?? ""
9471 }
9472 });
9473 toast(
9474 sprintf(
9475 /* translators: 1: plugin name, 2: new version */
9476 __("%1$s updated to %2$s.", "desktop-mode"),
9477 row.name || row.plugin,
9478 result.newVersion
9479 )
9480 );
9481 broadcast(PLUGINS_CHANGED_TOPIC, {
9482 source: SOURCE,
9483 plugin: row.plugin,
9484 action: "update"
9485 });
9486 void refreshFrameworkMenu();
9487 } catch (err) {
9488 const errCode = err?.code;
9489 const errMessage = err?.message;
9490 const coreUpToDateMessage = window.wp?.i18n?.__?.("The plugin is at the latest version.");
9491 const isUpToDate = errCode === "up_to_date" || !!coreUpToDateMessage && errMessage === coreUpToDateMessage;
9492 if (isUpToDate) {
9493 mergeRow({
9494 ...row,
9495 desktop_mode_update_available: {
9496 available: false,
9497 new_version: null,
9498 package: "",
9499 slug: row.desktop_mode_update_available?.slug ?? ""
9500 }
9501 });
9502 toast(
9503 sprintf(
9504 /* translators: %s: plugin name */
9505 __("%s is already up to date.", "desktop-mode"),
9506 row.name || row.plugin
9507 )
9508 );
9509 broadcast(PLUGINS_CHANGED_TOPIC, {
9510 source: SOURCE,
9511 plugin: row.plugin,
9512 action: "update"
9513 });
9514 } else {
9515 toast(
9516 sprintf(
9517 /* translators: 1: plugin name, 2: error message */
9518 __("Update of %1$s failed: %2$s", "desktop-mode"),
9519 row.name || row.plugin,
9520 describe(err)
9521 ),
9522 6e3
9523 );
9524 void reload();
9525 }
9526 void refreshFrameworkMenu();
9527 } finally {
9528 state.updating.delete(row.plugin);
9529 paintTable();
9530 }
9531 }
9532 async function runToggleAutoUpdate(row) {
9533 if (state.autoUpdating.has(row.plugin)) {
9534 return;
9535 }
9536 const meta = row.desktop_mode_auto_update;
9537 if (!meta || meta.forced !== null || !meta.supported) {
9538 return;
9539 }
9540 const wasEnabled = meta.enabled;
9541 const nextState = wasEnabled ? "disable" : "enable";
9542 state.autoUpdating.add(row.plugin);
9543 paintTable();
9544 try {
9545 await toggleAutoUpdate(row, nextState);
9546 mergeRow({
9547 ...row,
9548 desktop_mode_auto_update: {
9549 ...meta,
9550 enabled: !wasEnabled
9551 }
9552 });
9553 toast(
9554 wasEnabled ? sprintf(
9555 /* translators: %s: plugin name */
9556 __("Auto-updates disabled for %s.", "desktop-mode"),
9557 row.name || row.plugin
9558 ) : sprintf(
9559 /* translators: %s: plugin name */
9560 __("Auto-updates enabled for %s.", "desktop-mode"),
9561 row.name || row.plugin
9562 )
9563 );
9564 broadcast(PLUGINS_CHANGED_TOPIC, {
9565 source: SOURCE,
9566 plugin: row.plugin,
9567 action: "auto-update"
9568 });
9569 } catch (err) {
9570 toast(
9571 sprintf(
9572 /* translators: 1: plugin name, 2: error message */
9573 __(
9574 "Could not toggle auto-updates for %1$s: %2$s",
9575 "desktop-mode"
9576 ),
9577 row.name || row.plugin,
9578 describe(err)
9579 ),
9580 6e3
9581 );
9582 } finally {
9583 state.autoUpdating.delete(row.plugin);
9584 paintTable();
9585 }
9586 }
9587 async function runBulk(rows, action) {
9588 if (rows.length === 0) {
9589 return;
9590 }
9591 if (action === "delete") {
9592 const ok = await confirm({
9593 title: __("Delete selected plugins?", "desktop-mode"),
9594 message: sprintf(
9595 /* translators: %d: number of plugins */
9596 __(
9597 "Permanently delete %d plugin(s)? Their files will be removed from disk. This cannot be undone.",
9598 "desktop-mode"
9599 ),
9600 rows.length
9601 ),
9602 confirmLabel: __("Delete", "desktop-mode"),
9603 danger: true
9604 });
9605 if (!ok) {
9606 return;
9607 }
9608 }
9609 let succeeded = 0;
9610 let selfMutated = false;
9611 const failures = [];
9612 for (const row of rows) {
9613 try {
9614 if (action === "activate") {
9615 mergeRow(await activateInstalledPlugin(row));
9616 } else if (action === "deactivate") {
9617 mergeRow(await deactivateInstalledPlugin(row));
9618 } else if (action === "delete") {
9619 await deleteInstalledPlugin(row);
9620 state.rows = state.rows.filter((r) => r.plugin !== row.plugin);
9621 } else if (action === "update") {
9622 state.updating.add(row.plugin);
9623 paintTable();
9624 try {
9625 const result = await enqueueUpdateJob(
9626 () => updateInstalledPlugin(row)
9627 );
9628 mergeRow({
9629 ...row,
9630 version: result.newVersion,
9631 desktop_mode_update_available: {
9632 available: false,
9633 new_version: null,
9634 package: "",
9635 slug: row.desktop_mode_update_available?.slug ?? ""
9636 }
9637 });
9638 } finally {
9639 state.updating.delete(row.plugin);
9640 }
9641 }
9642 if ((action === "deactivate" || action === "delete") && isDesktopModeSelf(row.plugin)) {
9643 selfMutated = true;
9644 }
9645 succeeded++;
9646 } catch (err) {
9647 failures.push({ row, err });
9648 }
9649 }
9650 paintTable();
9651 table.clearSelection();
9652 if (selfMutated) {
9653 toast(
9654 action === "delete" ? __("Desktop Mode deleted. Reloading…", "desktop-mode") : __("Desktop Mode deactivated. Reloading…", "desktop-mode"),
9655 2e3
9656 );
9657 reloadOutOfDesktopMode();
9658 return;
9659 }
9660 if (succeeded > 0) {
9661 broadcast(PLUGINS_CHANGED_TOPIC, {
9662 source: SOURCE,
9663 action: "bulk"
9664 });
9665 }
9666 void refreshFrameworkMenu();
9667 let noun = "";
9668 if (action === "delete") {
9669 noun = __("deleted", "desktop-mode");
9670 } else if (action === "activate") {
9671 noun = __("activated", "desktop-mode");
9672 } else if (action === "update") {
9673 noun = __("updated", "desktop-mode");
9674 } else {
9675 noun = __("deactivated", "desktop-mode");
9676 }
9677 const summary = failures.length === 0 ? sprintf(
9678 /* translators: 1: count, 2: action verb (activated, deactivated, deleted) */
9679 __("%1$d plugin(s) %2$s.", "desktop-mode"),
9680 succeeded,
9681 noun
9682 ) : sprintf(
9683 /* translators: 1: success count, 2: failure count, 3: action verb */
9684 __("%1$d %3$s, %2$d failed.", "desktop-mode"),
9685 succeeded,
9686 failures.length,
9687 noun
9688 );
9689 toast(summary, 5e3);
9690 }
9691 function applyStatusOptimistic(row, next) {
9692 row.status = next;
9693 paintTable();
9694 }
9695 function mergeRow(updated) {
9696 const idx = state.rows.findIndex((r) => r.plugin === updated.plugin);
9697 if (idx >= 0) {
9698 state.rows[idx] = { ...state.rows[idx], ...updated };
9699 } else {
9700 state.rows.push(updated);
9701 }
9702 paintTable();
9703 }
9704 const unsubscribePluginsChanged = subscribe(
9705 PLUGINS_CHANGED_TOPIC,
9706 (payload) => {
9707 if (payload?.source === SOURCE) {
9708 return;
9709 }
9710 void reload();
9711 }
9712 );
9713 return () => {
9714 unsubscribePluginsChanged();
9715 table.removeEventListener("wpd-table-selection-change", selectionListener);
9716 host.replaceChildren();
9717 };
9718 }
9719 function formatSize(kb) {
9720 if (kb === null || kb === void 0) {
9721 return "";
9722 }
9723 if (kb < 1024) {
9724 return sprintf(
9725 /* translators: %d: size in kilobytes */
9726 __("%d KB", "desktop-mode"),
9727 kb
9728 );
9729 }
9730 const mb = kb / 1024;
9731 return sprintf(
9732 /* translators: %s: size in megabytes (one decimal) */
9733 __("%s MB", "desktop-mode"),
9734 mb.toFixed(1)
9735 );
9736 }
9737 function stripHtml(html2) {
9738 const tmp = document.createElement("div");
9739 tmp.innerHTML = html2;
9740 return tmp.textContent ?? "";
9741 }
9742 function describe(err) {
9743 if (err instanceof Error) {
9744 return err.message;
9745 }
9746 return String(err);
9747 }
9748 const _initial = {
9749 tab: null,
9750 requestedAt: 0
9751 };
9752 let _store = null;
9753 function getStore() {
9754 if (_store) {
9755 return _store;
9756 }
9757 const w = window;
9758 const factory = w.wp?.desktop?.createSharedStore;
9759 if (typeof factory !== "function") {
9760 return null;
9761 }
9762 _store = factory(
9763 "desktop-mode/plugins-window/tab-target",
9764 () => ({ ..._initial })
9765 );
9766 return _store;
9767 }
9768 function consumePluginsWindowTab() {
9769 const store = getStore();
9770 if (store) {
9771 const tab = store.state.tab;
9772 if (tab !== null) {
9773 store.state.tab = null;
9774 store.state.requestedAt = 0;
9775 store.notify();
9776 }
9777 return tab;
9778 }
9779 const w = window;
9780 const prev = w._wpdPluginsWindowTab;
9781 if (prev) {
9782 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
9783 return prev.tab;
9784 }
9785 return null;
9786 }
9787 function subscribePluginsWindowTab(cb) {
9788 const store = getStore();
9789 if (!store) {
9790 return () => {
9791 };
9792 }
9793 return store.subscribe((state) => cb({ ...state }));
9794 }
9795 function renderPluginsWindow(body) {
9796 const root = body.querySelector(
9797 "[data-desktop-mode-plugins-root]"
9798 );
9799 if (!root) {
9800 body.innerHTML = '<p style="padding:20px;color:var(--wpd-fg-muted,#666);">' + __("Plugins window template missing.", "desktop-mode") + "</p>";
9801 return;
9802 }
9803 const config = getConfig();
9804 const tabs = root.querySelector(
9805 "[data-desktop-mode-plugins-tabs]"
9806 );
9807 const installedHost = root.querySelector(
9808 "[data-desktop-mode-plugins-installed-host]"
9809 );
9810 let installedTeardown = null;
9811 if (installedHost) {
9812 if (config.caps.activate) {
9813 installedTeardown = mountInstalledView(installedHost);
9814 } else {
9815 installedHost.replaceChildren();
9816 const msg = document.createElement("p");
9817 msg.style.padding = "20px";
9818 msg.style.color = "var(--wpd-fg-muted, #666)";
9819 msg.textContent = __(
9820 "You do not have permission to manage plugins.",
9821 "desktop-mode"
9822 );
9823 installedHost.appendChild(msg);
9824 }
9825 }
9826 const browseHost = root.querySelector(
9827 "[data-desktop-mode-plugins-browse-host]"
9828 );
9829 const flyout = root.querySelector(
9830 "[data-desktop-mode-plugins-flyout]"
9831 );
9832 let browseTeardown = null;
9833 if (browseHost && config.caps.install) {
9834 browseTeardown = mountBrowseView(browseHost, flyout, body);
9835 }
9836 const featuredHost = root.querySelector(
9837 "[data-desktop-mode-plugins-featured-host]"
9838 );
9839 let featuredTeardown = null;
9840 if (featuredHost && config.caps.install) {
9841 featuredTeardown = mountFeaturedView(featuredHost, flyout);
9842 }
9843 const applyTab = (tab) => {
9844 if (!tabs) {
9845 return;
9846 }
9847 if ((tab === "browse" || tab === "featured") && !config.caps.install) {
9848 tabs.setAttribute("value", "installed");
9849 return;
9850 }
9851 tabs.setAttribute("value", tab);
9852 };
9853 const initialTab = consumePluginsWindowTab();
9854 if (initialTab) {
9855 applyTab(initialTab);
9856 }
9857 const unsubscribeTab = subscribePluginsWindowTab((state) => {
9858 if (state.tab) {
9859 applyTab(state.tab);
9860 }
9861 });
9862 const onClosed = (ev) => {
9863 const detail = ev.detail;
9864 if (detail?.windowId !== "desktop-mode-plugins") {
9865 return;
9866 }
9867 document.removeEventListener("desktop-mode-window-closed", onClosed);
9868 unsubscribeTab();
9869 if (installedTeardown) {
9870 installedTeardown();
9871 installedTeardown = null;
9872 }
9873 if (browseTeardown) {
9874 browseTeardown();
9875 browseTeardown = null;
9876 }
9877 if (featuredTeardown) {
9878 featuredTeardown();
9879 featuredTeardown = null;
9880 }
9881 };
9882 document.addEventListener("desktop-mode-window-closed", onClosed);
9883 void maybeShowIntro(config);
9884 }
9885 let _introShown = false;
9886 async function maybeShowIntro(config) {
9887 if (_introShown || config.introSeen) {
9888 return;
9889 }
9890 _introShown = true;
9891 try {
9892 const { showPluginsIntroDialog: showPluginsIntroDialog2 } = await Promise.resolve().then(() => introDialog);
9893 const result = await showPluginsIntroDialog2();
9894 if (result === "cancel") {
9895 _introShown = false;
9896 return;
9897 }
9898 void markIntroSeen(config);
9899 if (result === "settings") {
9900 openOsSettingsFeatures();
9901 }
9902 } catch {
9903 _introShown = false;
9904 }
9905 }
9906 async function markIntroSeen(config) {
9907 if (!config.introUrl) {
9908 return;
9909 }
9910 try {
9911 await trackedFetch(
9912 config.introUrl,
9913 {
9914 method: "POST",
9915 credentials: "same-origin",
9916 headers: {
9917 "Content-Type": "application/json",
9918 "X-WP-Nonce": config.restNonce
9919 },
9920 body: JSON.stringify({ slug: "plugins" })
9921 },
9922 {
9923 windowId: "desktop-mode-plugins",
9924 source: "plugins-window/intro"
9925 }
9926 );
9927 config.introSeen = true;
9928 } catch {
9929 }
9930 }
9931 function openOsSettingsFeatures() {
9932 const api2 = window.wp?.desktop;
9933 if (typeof api2?.openOsSettings === "function") {
9934 api2.openOsSettings({ tabId: "features" });
9935 }
9936 }
9937 const registry = window.desktopModeNativeWindows ?? (window.desktopModeNativeWindows = {});
9938 registry["desktop-mode-plugins"] = (body) => {
9939 renderPluginsWindow(body);
9940 };
9941 async function showPluginsIntroDialog() {
9942 return new Promise((resolve) => {
9943 const backdrop = document.createElement("div");
9944 backdrop.className = "desktop-mode-plugins-intro__backdrop";
9945 backdrop.setAttribute("role", "presentation");
9946 Object.assign(backdrop.style, {
9947 position: "fixed",
9948 inset: "0",
9949 background: "color-mix(in srgb, var(--wp-admin-theme-color, #1d2327) 60%, transparent)",
9950 backdropFilter: "blur(2px)",
9951 WebkitBackdropFilter: "blur(2px)",
9952 zIndex: "100000",
9953 display: "flex",
9954 alignItems: "center",
9955 justifyContent: "center",
9956 padding: "24px"
9957 });
9958 const dialog = document.createElement("div");
9959 dialog.setAttribute("role", "dialog");
9960 dialog.setAttribute("aria-modal", "true");
9961 dialog.setAttribute("aria-labelledby", "desktop-mode-plugins-intro-title");
9962 dialog.className = "desktop-mode-plugins-intro";
9963 Object.assign(dialog.style, {
9964 background: "var(--wp-admin-theme-bg, #fff)",
9965 color: "var(--wp-admin-theme-fg, #1d2327)",
9966 borderRadius: "14px",
9967 boxShadow: "0 24px 60px rgba(0,0,0,.28)",
9968 maxWidth: "560px",
9969 width: "100%",
9970 maxHeight: "90vh",
9971 overflow: "auto",
9972 padding: "28px 32px 24px",
9973 fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
9974 });
9975 dialog.innerHTML = renderDialogMarkup();
9976 backdrop.appendChild(dialog);
9977 document.body.appendChild(backdrop);
9978 const primaryBtn = dialog.querySelector(
9979 '[data-action="confirm"]'
9980 );
9981 const settingsBtn = dialog.querySelector(
9982 '[data-action="settings"]'
9983 );
9984 primaryBtn?.focus();
9985 let resolved = false;
9986 const cleanup = (result) => {
9987 if (resolved) {
9988 return;
9989 }
9990 resolved = true;
9991 document.removeEventListener("keydown", onKey, true);
9992 backdrop.remove();
9993 resolve(result);
9994 };
9995 const onKey = (e) => {
9996 if (e.key === "Escape") {
9997 e.preventDefault();
9998 cleanup("cancel");
9999 }
10000 };
10001 document.addEventListener("keydown", onKey, true);
10002 backdrop.addEventListener("click", (e) => {
10003 if (e.target === backdrop) {
10004 cleanup("cancel");
10005 }
10006 });
10007 primaryBtn?.addEventListener("click", () => cleanup("confirm"));
10008 settingsBtn?.addEventListener("click", () => cleanup("settings"));
10009 });
10010 }
10011 function renderDialogMarkup() {
10012 const title = __("Welcome to the new Plugins window", "desktop-mode");
10013 const lede = __(
10014 "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.",
10015 "desktop-mode"
10016 );
10017 const highlights = [
10018 __(
10019 "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.",
10020 "desktop-mode"
10021 ),
10022 __(
10023 "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.",
10024 "desktop-mode"
10025 ),
10026 __(
10027 "The detail flyout shows screenshots, the ratings histogram, recent reviews, the changelog and FAQ — all without leaving the window.",
10028 "desktop-mode"
10029 ),
10030 __(
10031 "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.",
10032 "desktop-mode"
10033 ),
10034 __(
10035 'The dock repaints LIVE after every install / activate / deactivate / delete. No reload, no stale tile, no "wait, did that work?".',
10036 "desktop-mode"
10037 ),
10038 __(
10039 "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.",
10040 "desktop-mode"
10041 )
10042 ];
10043 const li = (arr) => arr.map(
10044 (s) => `<li><span class="dot" aria-hidden="true"></span>${escapeHtml(
10045 s
10046 )}</li>`
10047 ).join("");
10048 return `
10049 <style>
10050 .desktop-mode-plugins-intro h2 {
10051 margin: 0 0 8px;
10052 font-size: 22px;
10053 font-weight: 600;
10054 letter-spacing: -0.01em;
10055 }
10056 .desktop-mode-plugins-intro p.lede {
10057 margin: 0 0 20px;
10058 color: var(--wp-admin-theme-fg-muted, #50575e);
10059 font-size: 14px;
10060 line-height: 1.5;
10061 }
10062 .desktop-mode-plugins-intro__list {
10063 list-style: none;
10064 margin: 0 0 22px;
10065 padding: 0;
10066 font-size: 14px;
10067 line-height: 1.5;
10068 }
10069 .desktop-mode-plugins-intro__list li {
10070 display: flex;
10071 align-items: flex-start;
10072 gap: 10px;
10073 padding: 6px 0;
10074 }
10075 .desktop-mode-plugins-intro__list .dot {
10076 flex: 0 0 auto;
10077 width: 6px;
10078 height: 6px;
10079 margin-top: 9px;
10080 border-radius: 50%;
10081 background: var(--wp-admin-theme-color, #2271b1);
10082 }
10083 .desktop-mode-plugins-intro__footer {
10084 display: flex;
10085 justify-content: flex-end;
10086 gap: 8px;
10087 margin-top: 8px;
10088 }
10089 .desktop-mode-plugins-intro__footer button {
10090 appearance: none;
10091 border: 1px solid var(--wp-admin-theme-border, #dcdcde);
10092 background: var(--wp-admin-theme-bg, #fff);
10093 color: inherit;
10094 padding: 8px 14px;
10095 border-radius: 6px;
10096 font-size: 13px;
10097 cursor: pointer;
10098 }
10099 .desktop-mode-plugins-intro__footer button.primary {
10100 border-color: var(--wp-admin-theme-color, #2271b1);
10101 background: var(--wp-admin-theme-color, #2271b1);
10102 color: #fff;
10103 font-weight: 500;
10104 }
10105 .desktop-mode-plugins-intro__footer button:hover {
10106 filter: brightness(1.05);
10107 }
10108 .desktop-mode-plugins-intro__footer button:focus-visible {
10109 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
10110 outline-offset: 2px;
10111 }
10112 </style>
10113 <h2 id="desktop-mode-plugins-intro-title">${escapeHtml(title)}</h2>
10114 <p class="lede">${escapeHtml(lede)}</p>
10115 <ul class="desktop-mode-plugins-intro__list">${li(highlights)}</ul>
10116 <div class="desktop-mode-plugins-intro__footer">
10117 <button type="button" data-action="settings">${escapeHtml(
10118 __("Take me to settings", "desktop-mode")
10119 )}</button>
10120 <button type="button" class="primary" data-action="confirm">${escapeHtml(
10121 __("Got it", "desktop-mode")
10122 )}</button>
10123 </div>
10124 `;
10125 }
10126 function escapeHtml(s) {
10127 const t = document.createElement("div");
10128 t.textContent = s;
10129 return t.innerHTML;
10130 }
10131 const introDialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
10132 __proto__: null,
10133 showPluginsIntroDialog
10134 }, Symbol.toStringTag, { value: "Module" }));
10135 })();
10136