PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.0
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 / os-settings-panel.js

os-settings-panel.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.0, at assets/js/os-settings-panel.js

5,728 lines 186.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function() {
2 "use strict";
3 const TEXT_DOMAIN = "desktop-mode";
4 function i18n() {
5 return window.wp?.i18n;
6 }
7 function __(text, domain = TEXT_DOMAIN) {
8 return i18n()?.__(text, domain) ?? text;
9 }
10 function sprintf(format, ...args) {
11 const impl = i18n()?.sprintf;
12 if (impl) {
13 return impl(format, ...args);
14 }
15 let i = 0;
16 return format.replace(/%[sd]/g, () => String(args[i++] ?? ""));
17 }
18 function html(strings, ...values) {
19 return { __wpdHtml: true, strings, values };
20 }
21 function isTemplateResult(v) {
22 return !!v && v.__wpdHtml === true;
23 }
24 const MARKER_PREFIX = "$$wpd$$";
25 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
26 function joinWithMarkers(strings) {
27 let out = strings[0];
28 for (let i = 1; i < strings.length; i++) {
29 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
30 }
31 return out;
32 }
33 const compiledCache = /* @__PURE__ */ new WeakMap();
34 function compile(strings) {
35 const cached = compiledCache.get(strings);
36 if (cached) {
37 return cached;
38 }
39 const template = document.createElement("template");
40 template.innerHTML = joinWithMarkers(strings);
41 const recipes = [];
42 const walk = (node, path) => {
43 if (node.nodeType === Node.ELEMENT_NODE) {
44 const el = node;
45 for (const attr of Array.from(el.attributes)) {
46 const rawName = attr.name;
47 const rawValue = attr.value;
48 const prefix = rawName[0];
49 if (MARKER_RE.test(rawValue)) {
50 MARKER_RE.lastIndex = 0;
51 if (prefix === "@") {
52 const match = MARKER_RE.exec(rawValue);
53 MARKER_RE.lastIndex = 0;
54 recipes.push({
55 path,
56 kind: "event",
57 name: rawName.slice(1),
58 valueIndex: match ? Number(match[1]) : 0
59 });
60 el.removeAttribute(rawName);
61 } else if (prefix === ".") {
62 const match = MARKER_RE.exec(rawValue);
63 MARKER_RE.lastIndex = 0;
64 recipes.push({
65 path,
66 kind: "prop",
67 name: rawName.slice(1),
68 valueIndex: match ? Number(match[1]) : 0
69 });
70 el.removeAttribute(rawName);
71 } else if (prefix === "?") {
72 const match = MARKER_RE.exec(rawValue);
73 MARKER_RE.lastIndex = 0;
74 recipes.push({
75 path,
76 kind: "bool",
77 name: rawName.slice(1),
78 valueIndex: match ? Number(match[1]) : 0
79 });
80 el.removeAttribute(rawName);
81 } else {
82 const fragments = [];
83 const indices = [];
84 let lastEnd = 0;
85 let m;
86 MARKER_RE.lastIndex = 0;
87 while ((m = MARKER_RE.exec(rawValue)) !== null) {
88 fragments.push(rawValue.slice(lastEnd, m.index));
89 indices.push(Number(m[1]));
90 lastEnd = m.index + m[0].length;
91 }
92 fragments.push(rawValue.slice(lastEnd));
93 recipes.push({
94 path,
95 kind: "attr",
96 name: rawName,
97 template: fragments,
98 valueIndices: indices
99 });
100 el.setAttribute(rawName, "");
101 }
102 }
103 }
104 }
105 const children = Array.from(node.childNodes);
106 let shift = 0;
107 for (let i = 0; i < children.length; i++) {
108 const child = children[i];
109 const liveIndex = i + shift;
110 if (child.nodeType === Node.TEXT_NODE) {
111 const text = child.textContent || "";
112 if (!MARKER_RE.test(text)) {
113 MARKER_RE.lastIndex = 0;
114 continue;
115 }
116 MARKER_RE.lastIndex = 0;
117 const parent = child.parentNode;
118 let lastEnd = 0;
119 let m;
120 const newNodes = [];
121 const newRecipes = [];
122 MARKER_RE.lastIndex = 0;
123 while ((m = MARKER_RE.exec(text)) !== null) {
124 if (m.index > lastEnd) {
125 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
126 }
127 const placeholder = document.createTextNode("");
128 newNodes.push(placeholder);
129 newRecipes.push({
130 path: [...path, liveIndex + newNodes.length - 1],
131 kind: "node",
132 valueIndex: Number(m[1])
133 });
134 lastEnd = m.index + m[0].length;
135 }
136 if (lastEnd < text.length) {
137 newNodes.push(document.createTextNode(text.slice(lastEnd)));
138 }
139 for (const nn of newNodes) {
140 parent.insertBefore(nn, child);
141 }
142 parent.removeChild(child);
143 shift += newNodes.length - 1;
144 recipes.push(...newRecipes);
145 } else {
146 walk(child, [...path, liveIndex]);
147 }
148 }
149 };
150 walk(template.content, []);
151 const buildParts = (fragment) => {
152 const out = [];
153 for (const r of recipes) {
154 let node = fragment;
155 for (const idx of r.path) {
156 node = node.childNodes[idx];
157 }
158 if (r.kind === "node") {
159 out.push({
160 kind: "node",
161 valueIndex: r.valueIndex,
162 child: {
163 anchor: node,
164 state: null
165 }
166 });
167 } else if (r.kind === "attr") {
168 out.push({
169 kind: "attr",
170 element: node,
171 name: r.name,
172 template: r.template,
173 valueIndices: r.valueIndices
174 });
175 } else if (r.kind === "event") {
176 out.push({
177 kind: "event",
178 valueIndex: r.valueIndex,
179 element: node,
180 name: r.name
181 });
182 } else if (r.kind === "prop") {
183 out.push({
184 kind: "prop",
185 valueIndex: r.valueIndex,
186 element: node,
187 name: r.name
188 });
189 } else if (r.kind === "bool") {
190 out.push({
191 kind: "bool",
192 valueIndex: r.valueIndex,
193 element: node,
194 name: r.name
195 });
196 }
197 }
198 return out;
199 };
200 const entry = { template, buildParts };
201 compiledCache.set(strings, entry);
202 return entry;
203 }
204 const mountState = /* @__PURE__ */ new WeakMap();
205 function render(result, container) {
206 const existing = mountState.get(container);
207 if (existing && existing.strings === result.strings) {
208 applyValues(existing.parts, result.values);
209 return;
210 }
211 const compiled = compile(result.strings);
212 const fragment = compiled.template.content.cloneNode(true);
213 const parts = compiled.buildParts(fragment);
214 while (container.firstChild) {
215 container.removeChild(container.firstChild);
216 }
217 container.appendChild(fragment);
218 applyValues(parts, result.values);
219 mountState.set(container, { strings: result.strings, parts });
220 }
221 function applyValues(parts, values) {
222 for (const part of parts) {
223 if (part.kind === "node") {
224 updateChildPart(part.child, values[part.valueIndex]);
225 } else if (part.kind === "attr") {
226 let composed = part.template[0];
227 for (let i = 0; i < part.valueIndices.length; i++) {
228 composed += formatText(values[part.valueIndices[i]]);
229 composed += part.template[i + 1];
230 }
231 if (composed !== part.last) {
232 part.last = composed;
233 if (composed === "") {
234 part.element.removeAttribute(part.name);
235 } else {
236 part.element.setAttribute(part.name, composed);
237 }
238 }
239 } else if (part.kind === "event") {
240 const next = values[part.valueIndex];
241 if (next !== part.current) {
242 if (part.current) {
243 part.element.removeEventListener(part.name, part.current);
244 }
245 if (next) {
246 part.element.addEventListener(part.name, next);
247 }
248 part.current = next;
249 }
250 } else if (part.kind === "prop") {
251 const next = values[part.valueIndex];
252 if (next !== part.last) {
253 part.last = next;
254 part.element[part.name] = next;
255 }
256 } else if (part.kind === "bool") {
257 const next = !!values[part.valueIndex];
258 if (next !== part.last) {
259 part.last = next;
260 if (next) {
261 part.element.setAttribute(part.name, "");
262 } else {
263 part.element.removeAttribute(part.name);
264 }
265 }
266 }
267 }
268 }
269 function updateChildPart(child, value) {
270 if (value === null || value === void 0 || value === false) {
271 if (child.state) {
272 disposeChildState(child.state);
273 child.state = null;
274 }
275 return;
276 }
277 if (Array.isArray(value)) {
278 updateArrayChild(child, value);
279 return;
280 }
281 if (isTemplateResult(value)) {
282 updateTemplateChild(child, value);
283 return;
284 }
285 if (value instanceof Node) {
286 updateNodeChild(child, value);
287 return;
288 }
289 updateTextChild(child, formatText(value));
290 }
291 function updateNodeChild(child, node) {
292 const old = child.state;
293 if (old?.shape === "node" && old.node === node) {
294 return;
295 }
296 if (old) {
297 disposeChildState(old);
298 }
299 insertBeforeAnchor(child, [node]);
300 child.state = { shape: "node", node };
301 }
302 function updateTextChild(child, text) {
303 const old = child.state;
304 if (old?.shape === "text") {
305 if (old.text !== text) {
306 old.node.textContent = text;
307 old.text = text;
308 }
309 return;
310 }
311 if (old) {
312 disposeChildState(old);
313 }
314 const node = document.createTextNode(text);
315 insertBeforeAnchor(child, [node]);
316 child.state = { shape: "text", node, text };
317 }
318 function updateTemplateChild(child, result) {
319 const old = child.state;
320 if (old?.shape === "template" && old.strings === result.strings) {
321 applyValues(old.parts, result.values);
322 return;
323 }
324 if (old) {
325 disposeChildState(old);
326 }
327 const compiled = compile(result.strings);
328 const fragment = compiled.template.content.cloneNode(true);
329 const parts = compiled.buildParts(fragment);
330 const topNodes = Array.from(fragment.childNodes);
331 insertBeforeAnchor(child, [fragment]);
332 applyValues(parts, result.values);
333 child.state = {
334 shape: "template",
335 strings: result.strings,
336 parts,
337 nodes: topNodes
338 };
339 }
340 function updateArrayChild(child, arr) {
341 const old = child.state;
342 if (old?.shape === "array" && old.entries.length === arr.length) {
343 for (let i = 0; i < arr.length; i++) {
344 updateChildPart(old.entries[i], arr[i]);
345 }
346 return;
347 }
348 if (old) {
349 disposeChildState(old);
350 }
351 const entries = [];
352 for (const v of arr) {
353 const entryAnchor = document.createTextNode("");
354 insertBeforeAnchor(child, [entryAnchor]);
355 const entry = { anchor: entryAnchor, state: null };
356 updateChildPart(entry, v);
357 entries.push(entry);
358 }
359 child.state = { shape: "array", entries };
360 }
361 function insertBeforeAnchor(child, nodes) {
362 const parent = child.anchor.parentNode;
363 if (!parent) {
364 return;
365 }
366 for (const node of nodes) {
367 parent.insertBefore(node, child.anchor);
368 }
369 }
370 function disposeChildState(state) {
371 if (state.shape === "text") {
372 state.node.remove();
373 return;
374 }
375 if (state.shape === "template") {
376 for (const node of state.nodes) {
377 if (node.parentNode) {
378 node.parentNode.removeChild(node);
379 }
380 }
381 return;
382 }
383 if (state.shape === "node") {
384 if (state.node.parentNode) {
385 state.node.parentNode.removeChild(state.node);
386 }
387 return;
388 }
389 for (const entry of state.entries) {
390 if (entry.state) {
391 disposeChildState(entry.state);
392 }
393 entry.anchor.remove();
394 }
395 }
396 function formatText(v) {
397 if (v === null || v === void 0 || v === false) {
398 return "";
399 }
400 return String(v);
401 }
402 const _Component = class _Component extends HTMLElement {
403 constructor() {
404 super();
405 this._renderScheduled = false;
406 this._propValues = {};
407 const ctor = this.constructor;
408 if (ctor.shadow) {
409 this.attachShadow({ mode: "open" });
410 this._renderRoot = this.shadowRoot;
411 } else {
412 this._renderRoot = this;
413 }
414 this._installPropAccessors();
415 }
416 static get observedAttributes() {
417 return this.props.map(kebab);
418 }
419 connectedCallback() {
420 this._adoptStyles();
421 this.requestUpdate();
422 }
423 attributeChangedCallback(name, oldValue, newValue) {
424 if (oldValue === newValue) {
425 return;
426 }
427 const prop = camel(name);
428 this._propValues[prop] = newValue;
429 this.requestUpdate();
430 }
431 /**
432 * Declarative class-name setter. Assign an array (or a
433 * space-separated string) and the host's `class` attribute is
434 * rewritten to match. Intended for programmatic styling — when
435 * a plugin has enqueued its own stylesheet and wants to apply
436 * one of those classes to a shell component:
437 *
438 * ```js
439 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
440 * // → <wpd-select class="my-plugin-brand is-active">
441 * ```
442 *
443 * The plain HTML `class="…"` attribute works just the same and
444 * is always preferred when writing markup by hand — this setter
445 * exists for the JS-API case where the caller has an array of
446 * conditional classes in hand.
447 *
448 * Getter returns the current `classList` as a plain array for
449 * symmetric read/write.
450 *
451 * @since 0.13.0
452 */
453 get classNames() {
454 return Array.from(this.classList);
455 }
456 set classNames(next) {
457 if (next === null || next === void 0) {
458 this.removeAttribute("class");
459 return;
460 }
461 const list2 = Array.isArray(next) ? next : String(next).split(/\s+/);
462 const cleaned = list2.map((s) => String(s).trim()).filter((s) => s !== "");
463 this.className = cleaned.join(" ");
464 }
465 /**
466 * Request a re-render explicitly. Components rarely need this —
467 * declare state via props + attribute observers and the render
468 * loop picks up changes automatically.
469 */
470 requestUpdate() {
471 this._scheduleRender();
472 }
473 /**
474 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
475 * by default (matches typical WC UX — events cross shadow
476 * boundaries, parents can listen without knowing about internal
477 * structure).
478 */
479 emit(name, detail) {
480 return this.dispatchEvent(
481 new CustomEvent(name, {
482 detail,
483 bubbles: true,
484 composed: true
485 })
486 );
487 }
488 // ------------------------------------------------------------------
489 // Internals
490 // ------------------------------------------------------------------
491 /**
492 * Wire every `static props` entry to a matched property getter +
493 * setter on the element. Setting the property reflects into the
494 * attribute (so downstream observers + CSS selectors see it);
495 * reading the property falls back to the attribute.
496 */
497 _installPropAccessors() {
498 const ctor = this.constructor;
499 for (const prop of ctor.props) {
500 if (Object.getOwnPropertyDescriptor(this, prop)) {
501 continue;
502 }
503 const attr = kebab(prop);
504 Object.defineProperty(this, prop, {
505 get: () => {
506 if (prop in this._propValues) {
507 return this._propValues[prop];
508 }
509 return this.getAttribute(attr);
510 },
511 set: (value) => {
512 let str;
513 if (value === null || value === void 0 || value === false) {
514 str = null;
515 } else if (value === true) {
516 str = "";
517 } else {
518 str = String(value);
519 }
520 this._propValues[prop] = str;
521 if (str === null) {
522 this.removeAttribute(attr);
523 } else {
524 this.setAttribute(attr, str);
525 }
526 this.requestUpdate();
527 },
528 enumerable: true,
529 configurable: true
530 });
531 }
532 }
533 /**
534 * Schedule a render on the next microtask. Multiple property
535 * assignments in the same tick collapse into a single render.
536 */
537 _scheduleRender() {
538 if (this._renderScheduled || !this.isConnected) {
539 return;
540 }
541 this._renderScheduled = true;
542 queueMicrotask(() => {
543 this._renderScheduled = false;
544 if (!this.isConnected) {
545 return;
546 }
547 render(this.render(), this._renderRoot);
548 });
549 }
550 /**
551 * Mount adoptable stylesheets onto the shadow root (via
552 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
553 * tag per def). No-op if `static styles` is empty.
554 */
555 _adoptStyles() {
556 const ctor = this.constructor;
557 if (ctor.styles.length === 0) {
558 return;
559 }
560 if (ctor.shadow && this.shadowRoot) {
561 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
562 this.shadowRoot.adoptedStyleSheets = sheets;
563 if (sheets.length !== ctor.styles.length) {
564 for (const s of ctor.styles) {
565 if (!s.sheet) {
566 const tag = document.createElement("style");
567 tag.textContent = s.cssText;
568 this.shadowRoot.appendChild(tag);
569 }
570 }
571 }
572 } else {
573 this._adoptLightStyles(ctor);
574 }
575 }
576 _adoptLightStyles(ctor) {
577 if (_Component._lightStylesAdopted.has(ctor)) {
578 return;
579 }
580 _Component._lightStylesAdopted.add(ctor);
581 for (const s of ctor.styles) {
582 const tag = document.createElement("style");
583 tag.dataset.wpdUi = this.tagName.toLowerCase();
584 tag.textContent = s.cssText;
585 document.head.appendChild(tag);
586 }
587 }
588 };
589 _Component.props = [];
590 _Component.styles = [];
591 _Component.shadow = true;
592 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
593 let Component = _Component;
594 function defineComponent(tag, ctor) {
595 if (customElements.get(tag)) {
596 return;
597 }
598 customElements.define(tag, ctor);
599 }
600 function kebab(s) {
601 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
602 }
603 function camel(s) {
604 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
605 }
606 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
607 try {
608 const s = new CSSStyleSheet();
609 return typeof s.replaceSync === "function";
610 } catch {
611 return false;
612 }
613 })();
614 function css(strings, ...values) {
615 let text = strings[0];
616 for (let i = 1; i < strings.length; i++) {
617 const v = values[i - 1];
618 if (typeof v === "string" || typeof v === "number") {
619 text += String(v);
620 } else if (v && v.__wpdCss) {
621 text += v.cssText;
622 } else {
623 throw new TypeError(
624 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
625 );
626 }
627 text += strings[i];
628 }
629 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
630 const sheet = new CSSStyleSheet();
631 sheet.replaceSync(text);
632 return { __wpdCss: true, sheet, cssText: text };
633 }
634 return { __wpdCss: true, sheet: null, cssText: text };
635 }
636 function computeAutoId(element) {
637 const parts = [];
638 const tabs = [];
639 let windowId = null;
640 let node = element.parentElement;
641 while (node) {
642 if (node === document.body || node === document.documentElement) {
643 break;
644 }
645 const id = node.id || "";
646 if (id.startsWith("wp-window-")) {
647 windowId = id.slice("wp-window-".length);
648 break;
649 }
650 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
651 const forValue = node.getAttribute("for");
652 if (forValue) {
653 tabs.unshift(forValue);
654 }
655 }
656 node = node.parentElement;
657 }
658 if (windowId) {
659 parts.push(slugify(windowId));
660 }
661 for (const tab of tabs) {
662 parts.push("tab-" + slugify(tab));
663 }
664 const label = element.getAttribute("label");
665 if (label) {
666 parts.push(slugify(label));
667 }
668 if (parts.length === 0) {
669 return "wpd-unnamed";
670 }
671 return "wpd-" + parts.filter((p) => p !== "").join("-");
672 }
673 function slugify(s) {
674 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
675 }
676 function ensureAutoId(element) {
677 if (element.id) {
678 return element.id;
679 }
680 const id = computeAutoId(element);
681 element.id = id;
682 return id;
683 }
684 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}`;
685 const _WpdButton = class _WpdButton extends Component {
686 render() {
687 const disabled = this.disabled !== null;
688 const type = this.type || "button";
689 return html`
690 <button part="button" type=${type} ?disabled=${disabled}>
691 <slot></slot>
692 </button>
693 `;
694 }
695 };
696 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
697 _WpdButton.styles = [styles$9];
698 _WpdButton.help = {
699 title: "Button",
700 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
701 status: "stable",
702 since: "0.9.0",
703 props: [
704 {
705 name: "variant",
706 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
707 default: "ghost",
708 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
709 },
710 {
711 name: "disabled",
712 type: "boolean attribute",
713 description: "Disable pointer + keyboard interaction and dim the chrome."
714 },
715 {
716 name: "type",
717 type: "'button' | 'submit' | 'reset'",
718 default: "button",
719 description: "Forwarded to the underlying native <button>."
720 },
721 {
722 name: "busy",
723 type: "boolean attribute",
724 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
725 },
726 {
727 name: "fill-cell",
728 type: "boolean attribute",
729 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
730 }
731 ],
732 slots: [{ name: "(default)", description: "Button label." }],
733 parts: [{ name: "button", description: "Underlying <button> element." }],
734 cssProps: [
735 { name: "--wpd-button-bg", description: "Background color." },
736 { name: "--wpd-button-fg", description: "Text color." },
737 { name: "--wpd-button-border", description: "Border shorthand." },
738 { name: "--wpd-button-border-radius", default: "6px" },
739 { name: "--wpd-button-padding", default: "6px 12px" },
740 {
741 name: "--wpd-button-min-height",
742 description: "Minimum height when fill-cell is set."
743 }
744 ],
745 example: html`
746 <wpd-cluster gap="8">
747 <wpd-button variant="primary">Primary</wpd-button>
748 <wpd-button variant="secondary">Secondary</wpd-button>
749 <wpd-button variant="ghost">Ghost</wpd-button>
750 <wpd-button variant="danger">Danger</wpd-button>
751 <wpd-button variant="link">Link</wpd-button>
752 </wpd-cluster>
753 `
754 };
755 let WpdButton = _WpdButton;
756 defineComponent("wpd-button", WpdButton);
757 const styles$8 = css`:host{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var( --desktop-mode-text,#1d2327 );cursor:pointer}label{display:inline-flex;align-items:center;gap:6px;cursor:pointer}input[ type='checkbox' ]{accent-color:var( --wp-admin-theme-color,#2271b1 );cursor:pointer}`;
758 const _WpdCheckboxLabel = class _WpdCheckboxLabel extends Component {
759 render() {
760 const label = this.label || "";
761 const checked = this.checked !== null;
762 return html`
763 <label>
764 <input
765 type="checkbox"
766 ?checked=${checked}
767 @change=${(e) => this._onChange(e)}
768 />
769 <span class="wpd-checkbox-label__text">${label}</span>
770 </label>
771 `;
772 }
773 _onChange(e) {
774 const next = e.target.checked;
775 if (next) {
776 this.setAttribute("checked", "");
777 } else {
778 this.removeAttribute("checked");
779 }
780 this.emit("wpd-checkbox-change", { checked: next });
781 }
782 };
783 _WpdCheckboxLabel.props = ["label", "checked"];
784 _WpdCheckboxLabel.styles = [styles$8];
785 _WpdCheckboxLabel.help = {
786 title: "Checkbox label",
787 summary: "Opinionated label-row variant of <wpd-checkbox>: label text + checkbox in a single aligned row. Use when you want the shipped layout without any layout work.",
788 status: "stable",
789 since: "0.9.0",
790 props: [
791 {
792 name: "label",
793 type: "string",
794 description: "Visible label text, paired with the checkbox via a native <label>."
795 },
796 {
797 name: "checked",
798 type: "boolean attribute",
799 description: "Reflects and controls the checked state."
800 }
801 ],
802 events: [
803 {
804 name: "wpd-checkbox-change",
805 description: "Fires when the user toggles the checkbox.",
806 detail: "{ checked: boolean }"
807 }
808 ],
809 cssProps: [
810 { name: "--desktop-mode-text", description: "Label colour." }
811 ],
812 example: html`
813 <wpd-checkbox-label label="Reduce motion" checked></wpd-checkbox-label>
814 `
815 };
816 let WpdCheckboxLabel = _WpdCheckboxLabel;
817 defineComponent("wpd-checkbox-label", WpdCheckboxLabel);
818 const styles$7 = css`:host{display:inline-flex;align-items:center;gap:8px;font-size:12px;color:var( --desktop-mode-muted,#646970 )}label{display:inline-flex;align-items:center;gap:8px}input[ type='color' ]{width:28px;height:28px;padding:0;border:1px solid var( --desktop-mode-border,#c3c4c7 );border-radius:6px;background:transparent;cursor:pointer}:host( [ variant='block' ] ){display:flex;width:100%}:host( [ variant='block' ] ) label{display:flex;flex:1;align-items:center}:host( [ variant='block' ] ) input[ type='color' ]{flex:1;width:auto;height:32px}:host( [ variant='block' ] ) input[ type='color' ]::-webkit-color-swatch-wrapper{padding:2px}:host( [ variant='block' ] ) input[ type='color' ]::-webkit-color-swatch{border:none;border-radius:2px}`;
819 const _WpdColorField = class _WpdColorField extends Component {
820 render() {
821 const label = this.label || "";
822 const value = this.value || "#000000";
823 return html`
824 <label>
825 <span class="wpd-color-field__label">${label}</span>
826 <input
827 type="color"
828 .value=${value}
829 @input=${(e) => this._onInput(e)}
830 />
831 </label>
832 `;
833 }
834 _onInput(e) {
835 const input = e.target;
836 this.value = input.value;
837 this.emit("wpd-color-change", { value: input.value });
838 }
839 };
840 _WpdColorField.props = ["label", "value", "variant"];
841 _WpdColorField.styles = [styles$7];
842 _WpdColorField.help = {
843 title: "Color field",
844 summary: "Label + native color input. Reflects the value attribute both ways and emits wpd-color-change live on every edit (no debounce — callers debounce upstream).",
845 status: "stable",
846 since: "0.9.0",
847 props: [
848 {
849 name: "label",
850 type: "string",
851 description: "Visible label rendered next to the swatch."
852 },
853 {
854 name: "value",
855 type: "CSS hex color",
856 default: "#000000",
857 description: "Current color. Two-way reflected with the native picker."
858 },
859 {
860 name: "variant",
861 type: "string",
862 description: "Optional visual variant hint for the stylesheet."
863 }
864 ],
865 events: [
866 {
867 name: "wpd-color-change",
868 description: "Fires on every user edit.",
869 detail: "{ value: string }"
870 }
871 ],
872 cssProps: [
873 { name: "--desktop-mode-border", description: "Swatch outline." },
874 { name: "--desktop-mode-muted", description: "Label colour." }
875 ],
876 example: html`
877 <wpd-color-field label="Accent" value="#8b5cf6"></wpd-color-field>
878 `
879 };
880 let WpdColorField = _WpdColorField;
881 defineComponent("wpd-color-field", WpdColorField);
882 const styles$6 = 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}`;
883 let _cache = null;
884 function parseCssContentToChar(raw) {
885 let value = raw.trim();
886 if (value === "") {
887 return null;
888 }
889 if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
890 value = value.slice(1, -1);
891 }
892 const escaped = value.match(/^\\([0-9a-f]{1,6})\s?$/i);
893 if (escaped) {
894 return String.fromCodePoint(parseInt(escaped[1], 16));
895 }
896 return value || null;
897 }
898 function buildMap() {
899 const map = /* @__PURE__ */ new Map();
900 if (typeof document === "undefined") {
901 return map;
902 }
903 const sheets = Array.from(document.styleSheets ?? []);
904 for (const sheet of sheets) {
905 let rules = null;
906 try {
907 rules = sheet.cssRules;
908 } catch {
909 continue;
910 }
911 if (!rules) {
912 continue;
913 }
914 for (const rule of Array.from(rules)) {
915 const styleRule = rule;
916 if (!styleRule || !styleRule.selectorText) {
917 continue;
918 }
919 const match = styleRule.selectorText.match(
920 /\.dashicons-([a-z0-9-]+)::?before/i
921 );
922 if (!match) {
923 continue;
924 }
925 const content = styleRule.style?.content;
926 if (!content) {
927 continue;
928 }
929 const char = parseCssContentToChar(content);
930 if (char) {
931 map.set(match[1], char);
932 }
933 }
934 }
935 return map;
936 }
937 function resolveDashicon(name) {
938 if (!_cache) {
939 _cache = buildMap();
940 }
941 const slug = name.startsWith("dashicons-") ? name.slice("dashicons-".length) : name;
942 return _cache.get(slug) ?? null;
943 }
944 function refreshDashiconCache() {
945 _cache = buildMap();
946 }
947 let _scheduled = false;
948 function primeOnLoad() {
949 if (_scheduled || typeof window === "undefined") {
950 return;
951 }
952 _scheduled = true;
953 const refresh = () => {
954 refreshDashiconCache();
955 };
956 if (document.readyState === "loading") {
957 document.addEventListener("DOMContentLoaded", refresh, { once: true });
958 }
959 window.addEventListener("load", refresh, { once: true });
960 }
961 primeOnLoad();
962 const _WpdIcon = class _WpdIcon extends Component {
963 render() {
964 const rawName = this.name || "";
965 const slug = rawName.startsWith("dashicons-") ? rawName.slice("dashicons-".length) : rawName;
966 const size = this.size;
967 if (size && /^\d+$/.test(size)) {
968 this.style.setProperty("--wpd-icon-size", `${size}px`);
969 }
970 const char = resolveDashicon(slug);
971 if (char) {
972 return html`<span
973 class="wpd-icon__glyph wpd-icon__glyph--char dashicons dashicons-${slug}"
974 aria-hidden="true"
975 >${char}</span>`;
976 }
977 return html`<span
978 class="wpd-icon__glyph dashicons dashicons-${slug}"
979 aria-hidden="true"
980 ></span>`;
981 }
982 };
983 _WpdIcon.props = ["name", "size"];
984 _WpdIcon.styles = [styles$6];
985 _WpdIcon.help = {
986 title: "Icon",
987 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.',
988 status: "stable",
989 since: "0.10.0",
990 props: [
991 {
992 name: "name",
993 type: "string",
994 description: "Dashicon identifier, with or without the `dashicons-` prefix."
995 },
996 {
997 name: "size",
998 type: "integer (px)",
999 default: "16",
1000 description: "Glyph size in pixels."
1001 }
1002 ],
1003 cssProps: [
1004 { name: "--wpd-icon-size", default: "16px" }
1005 ],
1006 example: html`
1007 <wpd-cluster gap="8" align="center">
1008 <wpd-icon name="admin-post"></wpd-icon>
1009 <wpd-icon name="calculator" size="20"></wpd-icon>
1010 <wpd-icon name="dashicons-star-filled" size="32"></wpd-icon>
1011 </wpd-cluster>
1012 `
1013 };
1014 let WpdIcon = _WpdIcon;
1015 defineComponent("wpd-icon", WpdIcon);
1016 const styles$5 = 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}`;
1017 const _WpdEmptyState = class _WpdEmptyState extends Component {
1018 render() {
1019 const icon = this.icon || "";
1020 const heading = this.heading || "";
1021 const description = this.description || "";
1022 return html`
1023 ${icon ? html`<wpd-icon
1024 class="wpd-empty-state__icon"
1025 name=${icon}
1026 size="28"
1027 ></wpd-icon>` : null}
1028 <h3 class="wpd-empty-state__heading">${heading}</h3>
1029 <p class="wpd-empty-state__description">${description}</p>
1030 <div class="wpd-empty-state__cta">
1031 <slot name="cta"></slot>
1032 </div>
1033 <slot></slot>
1034 `;
1035 }
1036 };
1037 _WpdEmptyState.props = ["icon", "heading", "description"];
1038 _WpdEmptyState.styles = [styles$5];
1039 _WpdEmptyState.help = {
1040 title: "Empty state",
1041 summary: 'Centered placeholder for "nothing here yet" UI: icon + heading + description + optional CTA. A canonical shape so empty states look consistent across the shell.',
1042 status: "stable",
1043 since: "0.10.0",
1044 props: [
1045 {
1046 name: "icon",
1047 type: "string (dashicons slug)",
1048 description: "Dashicons identifier (with or without the dashicons- prefix)."
1049 },
1050 {
1051 name: "heading",
1052 type: "string",
1053 description: "Bold first line."
1054 },
1055 {
1056 name: "description",
1057 type: "string",
1058 description: "Secondary paragraph below the heading."
1059 }
1060 ],
1061 slots: [
1062 { name: "cta", description: "Call-to-action button row below the description." },
1063 { name: "(default)", description: "Any additional content rendered after the CTA." }
1064 ],
1065 cssProps: [
1066 { name: "--desktop-mode-text", description: "Heading colour." },
1067 { name: "--desktop-mode-muted", description: "Description colour." },
1068 { name: "--wpd-empty-state-fg" },
1069 { name: "--wpd-empty-state-icon-color" }
1070 ],
1071 example: html`
1072 <wpd-empty-state
1073 icon="admin-plugins"
1074 heading="No plugins installed yet"
1075 description="Install a plugin to see it here."
1076 >
1077 <wpd-button slot="cta" variant="primary">Browse plugins</wpd-button>
1078 </wpd-empty-state>
1079 `
1080 };
1081 let WpdEmptyState = _WpdEmptyState;
1082 defineComponent("wpd-empty-state", WpdEmptyState);
1083 const styles$4 = css`:host{display:flex;flex-direction:column;gap:var( --wpd-panel-gap,12px );padding:var( --wpd-panel-padding,16px );box-sizing:border-box}:host( [ hidden ] ){display:none}`;
1084 const _WpdPanel = class _WpdPanel extends Component {
1085 render() {
1086 const gap = this.gap;
1087 const padding = this.padding;
1088 if (gap && /^\d+$/.test(gap)) {
1089 this.style.setProperty("--wpd-panel-gap", `${gap}px`);
1090 }
1091 if (padding && /^\d+$/.test(padding)) {
1092 this.style.setProperty("--wpd-panel-padding", `${padding}px`);
1093 }
1094 return html`<slot></slot>`;
1095 }
1096 };
1097 _WpdPanel.props = ["gap", "padding"];
1098 _WpdPanel.styles = [styles$4];
1099 _WpdPanel.help = {
1100 title: "Panel",
1101 summary: "Padded, flex-column container matching the default inset and rhythm of a native-window body. Opt-in for the OS-Settings-style padded layout.",
1102 status: "stable",
1103 since: "0.10.0",
1104 props: [
1105 {
1106 name: "gap",
1107 type: "integer (px)",
1108 default: "12",
1109 description: "Space between children."
1110 },
1111 {
1112 name: "padding",
1113 type: "integer (px)",
1114 default: "16",
1115 description: "Inset around children. Pass 0 to drop the inset."
1116 }
1117 ],
1118 slots: [{ name: "(default)", description: "Panel body." }],
1119 cssProps: [
1120 { name: "--wpd-panel-gap", default: "12px" },
1121 { name: "--wpd-panel-padding", default: "16px" }
1122 ],
1123 example: html`
1124 <wpd-panel>
1125 <wpd-section heading="Look">Panel section A</wpd-section>
1126 <wpd-section heading="Feel">Panel section B</wpd-section>
1127 </wpd-panel>
1128 `
1129 };
1130 let WpdPanel = _WpdPanel;
1131 defineComponent("wpd-panel", WpdPanel);
1132 const styles$3 = css`:host{display:flex;align-items:center;gap:10px;font-size:12px;color:var( --desktop-mode-muted,#646970 )}input[ type='range' ]{flex:1;accent-color:var( --wp-admin-theme-color,#2271b1 )}.wpd-range-field__value{min-width:3ch;text-align:end;font-variant-numeric:tabular-nums;color:var( --desktop-mode-text,#1d2327 )}`;
1133 const _WpdRangeField = class _WpdRangeField extends Component {
1134 render() {
1135 const label = this.label || "";
1136 const value = this.value || "0";
1137 const min = this.min || "0";
1138 const max = this.max || "100";
1139 const step = this.step || "1";
1140 const suffix = this.suffix || "";
1141 return html`
1142 <label class="wpd-range-field__label">${label}</label>
1143 <input
1144 type="range"
1145 min=${min}
1146 max=${max}
1147 step=${step}
1148 .value=${value}
1149 @input=${(e) => this._onInput(e)}
1150 />
1151 <span class="wpd-range-field__value">${value}${suffix}</span>
1152 `;
1153 }
1154 _onInput(e) {
1155 const input = e.target;
1156 const n = parseFloat(input.value);
1157 if (!Number.isFinite(n)) {
1158 return;
1159 }
1160 this.value = String(n);
1161 this.emit("wpd-range-change", { value: n });
1162 }
1163 };
1164 _WpdRangeField.props = ["label", "value", "min", "max", "step", "suffix"];
1165 _WpdRangeField.styles = [styles$3];
1166 _WpdRangeField.help = {
1167 title: "Range field",
1168 summary: "Label + range slider + live numeric readout. Emits wpd-range-change with an already-parsed number.",
1169 status: "stable",
1170 since: "0.9.0",
1171 props: [
1172 {
1173 name: "label",
1174 type: "string",
1175 description: "Visible label above the slider."
1176 },
1177 {
1178 name: "value",
1179 type: "number (string)",
1180 default: "0",
1181 description: "Current slider value."
1182 },
1183 {
1184 name: "min",
1185 type: "number (string)",
1186 default: "0",
1187 description: "Lower bound of the slider range."
1188 },
1189 {
1190 name: "max",
1191 type: "number (string)",
1192 default: "100",
1193 description: "Upper bound of the slider range."
1194 },
1195 {
1196 name: "step",
1197 type: "number (string)",
1198 default: "1",
1199 description: "Slider step granularity."
1200 },
1201 {
1202 name: "suffix",
1203 type: "string",
1204 description: 'Text appended to the readout (e.g. "px", "%").'
1205 }
1206 ],
1207 events: [
1208 {
1209 name: "wpd-range-change",
1210 description: "Fires on every slider movement.",
1211 detail: "{ value: number }"
1212 }
1213 ],
1214 cssProps: [
1215 { name: "--desktop-mode-text", description: "Readout + label colour." },
1216 { name: "--desktop-mode-muted", description: "Secondary colour." }
1217 ],
1218 example: html`
1219 <wpd-range-field
1220 label="Dock size"
1221 value="48"
1222 min="32"
1223 max="80"
1224 step="4"
1225 suffix="px"
1226 ></wpd-range-field>
1227 `
1228 };
1229 let WpdRangeField = _WpdRangeField;
1230 defineComponent("wpd-range-field", WpdRangeField);
1231 const styles$2 = css`:host{display:block;margin-block-end:28px}:host( [ hidden ] ){display:none}.wpd-section__heading{margin:0 0 2px;font-size:14px;font-weight:600;color:var( --desktop-mode-text,#1d2327 )}.wpd-section__description{margin:0 0 14px;font-size:12px;color:var( --desktop-mode-muted,#646970 );line-height:1.45}.wpd-section__description:empty{display:none}:host( [ stack ] ) .wpd-section__body{display:flex;flex-direction:column;gap:var( --wpd-section-gap,12px )}`;
1232 const _WpdSection = class _WpdSection extends Component {
1233 render() {
1234 const heading = this.heading || "";
1235 const description = this.description || "";
1236 return html`
1237 <h3 class="wpd-section__heading">${heading}</h3>
1238 <p class="wpd-section__description">${description}</p>
1239 <div class="wpd-section__body"><slot></slot></div>
1240 `;
1241 }
1242 };
1243 _WpdSection.props = ["heading", "description", "stack"];
1244 _WpdSection.styles = [styles$2];
1245 _WpdSection.help = {
1246 title: "Section",
1247 summary: "Titled panel with heading + description + a body slot. The canonical OS Settings section wrapper.",
1248 status: "stable",
1249 since: "0.9.0",
1250 props: [
1251 {
1252 name: "heading",
1253 type: "string",
1254 description: "Section title, rendered as an <h3>."
1255 },
1256 {
1257 name: "description",
1258 type: "string",
1259 description: "Secondary descriptive paragraph below the heading."
1260 },
1261 {
1262 name: "stack",
1263 type: "boolean",
1264 description: "When present, the default slot becomes a flex column with a consistent gap (--wpd-section-gap, default 12px) between children. Opt-in — existing callers whose slotted controls ship their own margin stay unchanged. Recommended for third-party settings tabs and any new surface."
1265 }
1266 ],
1267 slots: [
1268 { name: "(default)", description: "Section body content." }
1269 ],
1270 cssProps: [
1271 { name: "--desktop-mode-text", description: "Heading colour." },
1272 { name: "--desktop-mode-muted", description: "Description colour." }
1273 ],
1274 example: html`
1275 <wpd-section
1276 heading="Wallpaper"
1277 description="Pick a backdrop for the desktop."
1278 >
1279 <wpd-swatch-grid>
1280 <wpd-swatch value="a" preview="#b1e7b9"></wpd-swatch>
1281 <wpd-swatch value="b" preview="#e7b1c9"></wpd-swatch>
1282 </wpd-swatch-grid>
1283 </wpd-section>
1284 `
1285 };
1286 let WpdSection = _WpdSection;
1287 defineComponent("wpd-section", WpdSection);
1288 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}`;
1289 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}`;
1290 const _WpdSegment = class _WpdSegment extends Component {
1291 render() {
1292 this.setAttribute("role", "radio");
1293 return html`
1294 <button type="button" @click=${() => this._onPick()}>
1295 <slot></slot>
1296 </button>
1297 `;
1298 }
1299 _onPick() {
1300 this.emit("wpd-segment-pick", {
1301 value: this.value
1302 });
1303 }
1304 };
1305 _WpdSegment.props = ["value"];
1306 _WpdSegment.styles = [segmentStyles];
1307 _WpdSegment.help = {
1308 title: "Segment",
1309 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
1310 status: "stable",
1311 since: "0.9.0",
1312 props: [
1313 {
1314 name: "value",
1315 type: "string",
1316 description: "Identifier this segment contributes to the parent group selection."
1317 }
1318 ],
1319 slots: [
1320 { name: "(default)", description: "Visible segment label." }
1321 ],
1322 events: [
1323 {
1324 name: "wpd-segment-pick",
1325 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
1326 detail: "{ value: string }"
1327 }
1328 ]
1329 };
1330 let WpdSegment = _WpdSegment;
1331 defineComponent("wpd-segment", WpdSegment);
1332 const _WpdSegmented = class _WpdSegmented extends Component {
1333 connectedCallback() {
1334 super.connectedCallback();
1335 this.addEventListener("wpd-segment-pick", (e) => {
1336 const detail = e.detail;
1337 e.stopPropagation();
1338 this.value = detail.value;
1339 this.emit("wpd-pick", { value: detail.value });
1340 });
1341 }
1342 /**
1343 * Declarative item-list setter. Replaces the existing
1344 * `<wpd-segment>` children with a fresh set built from a
1345 * `{ value, label }` array; preserves the current selection
1346 * when the value still matches an entry, otherwise falls back
1347 * to the first item.
1348 *
1349 * Collapses the pre-0.11 imperative dance (clear children,
1350 * `createElement`, set `textContent`, `appendChild`, then
1351 * `setAttribute('value', …)` on the group — order matters) to
1352 * a single assignment:
1353 *
1354 * ```js
1355 * segmented.items = [
1356 * { value: 'm', label: 'm' },
1357 * { value: 'km', label: 'km' },
1358 * ];
1359 * ```
1360 *
1361 * @since 0.11.0
1362 */
1363 set items(list2) {
1364 const existing = this.querySelectorAll(":scope > wpd-segment");
1365 for (const el of Array.from(existing)) {
1366 el.remove();
1367 }
1368 for (const item of list2) {
1369 const seg = document.createElement("wpd-segment");
1370 seg.setAttribute("value", item.value);
1371 seg.textContent = item.label;
1372 this.appendChild(seg);
1373 }
1374 const current = this.value;
1375 const stillValid = current !== null && list2.some((i) => i.value === current);
1376 if (!stillValid && list2.length > 0) {
1377 this.value = list2[0].value;
1378 } else {
1379 this.requestUpdate();
1380 }
1381 }
1382 render() {
1383 const label = this.label || "";
1384 if (label) {
1385 this.setAttribute("aria-label", label);
1386 }
1387 this.setAttribute("role", "radiogroup");
1388 const current = this.value;
1389 queueMicrotask(() => {
1390 const segs = this.querySelectorAll("wpd-segment");
1391 for (const seg of Array.from(segs)) {
1392 const v = seg.getAttribute("value");
1393 seg.setAttribute(
1394 "aria-checked",
1395 v === current ? "true" : "false"
1396 );
1397 }
1398 });
1399 return html`<slot></slot>`;
1400 }
1401 };
1402 _WpdSegmented.props = ["value", "label"];
1403 _WpdSegmented.styles = [segmentedStyles];
1404 _WpdSegmented.help = {
1405 title: "Segmented",
1406 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
1407 status: "stable",
1408 since: "0.9.0",
1409 props: [
1410 {
1411 name: "value",
1412 type: "string",
1413 description: "Currently selected segment value. Mirrored onto child aria-checked."
1414 },
1415 {
1416 name: "label",
1417 type: "string",
1418 description: "aria-label for the radiogroup."
1419 }
1420 ],
1421 slots: [
1422 { name: "(default)", description: '<wpd-segment value="…"> children.' }
1423 ],
1424 events: [
1425 {
1426 name: "wpd-pick",
1427 description: "Fires when the selected segment changes.",
1428 detail: "{ value: string }"
1429 }
1430 ],
1431 cssProps: [
1432 { name: "--desktop-mode-window-bg", description: "Pill background." },
1433 { name: "--desktop-mode-text", description: "Active label colour." },
1434 { name: "--desktop-mode-muted", description: "Inactive label colour." }
1435 ],
1436 example: html`
1437 <wpd-segmented value="md" label="Dock size">
1438 <wpd-segment value="sm">Small</wpd-segment>
1439 <wpd-segment value="md">Medium</wpd-segment>
1440 <wpd-segment value="lg">Large</wpd-segment>
1441 </wpd-segmented>
1442 `
1443 };
1444 let WpdSegmented = _WpdSegmented;
1445 defineComponent("wpd-segmented", WpdSegmented);
1446 const selectStyles = 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-select__label{font-size:12px;color:var( --desktop-mode-muted,#646970 )}.wpd-select__wrap{position:relative;display:flex;align-items:center;width:100%}select{appearance:none;-webkit-appearance:none;display:block;width:100%;min-width:0;padding:7px 28px 7px 12px;background:rgba( 0,0,0,0.05 );border:1px solid transparent;border-radius:7px;font:inherit;font-size:13px;color:var( --desktop-mode-text,#1d2327 );cursor:pointer;transition:background-color 0.12s ease,border-color 0.12s ease,box-shadow 0.12s ease}select:hover{background:rgba( 0,0,0,0.08 )}select:focus-visible{outline:none;border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 1px var( --wp-admin-theme-color,#2271b1 )}select:disabled{opacity:0.5;cursor:not-allowed}.wpd-select__chevron{position:absolute;inset-inline-end:10px;top:50%;transform:translateY( -50% );pointer-events:none;color:var( --desktop-mode-muted,#646970 );display:inline-block}select:hover ~ .wpd-select__chevron,select:focus-visible ~ .wpd-select__chevron{color:var( --desktop-mode-text,#1d2327 )}`;
1447 const optionStyles = css`:host{display:none}`;
1448 const _WpdOption = class _WpdOption extends Component {
1449 render() {
1450 return html``;
1451 }
1452 };
1453 _WpdOption.props = ["value", "disabled"];
1454 _WpdOption.styles = [optionStyles];
1455 _WpdOption.help = {
1456 title: "Option",
1457 summary: "Opaque data carrier for <wpd-select>. Carries its identifier in `value` and its visible label in textContent. Not rendered directly — the parent reads these and builds a native <select>.",
1458 status: "stable",
1459 since: "0.11.0",
1460 props: [
1461 {
1462 name: "value",
1463 type: "string",
1464 description: "Option identifier read by the parent <wpd-select>."
1465 },
1466 {
1467 name: "disabled",
1468 type: "boolean attribute",
1469 description: "Renders the option disabled in the parent <select>."
1470 }
1471 ],
1472 slots: [
1473 { name: "(default)", description: "Label text read from textContent." }
1474 ]
1475 };
1476 let WpdOption = _WpdOption;
1477 defineComponent("wpd-option", WpdOption);
1478 const _WpdSelect = class _WpdSelect extends Component {
1479 constructor() {
1480 super(...arguments);
1481 this._optionObserver = null;
1482 }
1483 /**
1484 * Declarative item-list setter. Replaces the existing
1485 * `<wpd-option>` children with a fresh set; preserves `value`
1486 * when it still matches, otherwise clears to the placeholder.
1487 *
1488 * Same shape as the setter on `<wpd-segmented>` so callers can
1489 * swap tag names (segmented ↔ select) without touching the
1490 * populate code when an option list outgrows the pill bar.
1491 *
1492 * ```js
1493 * select.items = [
1494 * { value: 'eur', label: 'Euro' },
1495 * { value: 'usd', label: 'US Dollar' },
1496 * ];
1497 * ```
1498 *
1499 * @since 0.11.0
1500 */
1501 set items(list2) {
1502 const existing = this.querySelectorAll(":scope > wpd-option");
1503 for (const el of Array.from(existing)) {
1504 el.remove();
1505 }
1506 for (const item of list2) {
1507 const opt = document.createElement("wpd-option");
1508 opt.setAttribute("value", item.value);
1509 opt.textContent = item.label;
1510 this.appendChild(opt);
1511 }
1512 const current = this.value;
1513 const stillValid = current !== null && list2.some((i) => i.value === current);
1514 if (!stillValid && list2.length > 0) {
1515 this.value = list2[0].value;
1516 }
1517 this.requestUpdate();
1518 }
1519 connectedCallback() {
1520 super.connectedCallback();
1521 ensureAutoId(this);
1522 this._optionObserver = new MutationObserver(() => this.requestUpdate());
1523 this._optionObserver.observe(this, {
1524 childList: true,
1525 subtree: true,
1526 attributes: true,
1527 attributeFilter: ["value", "disabled"],
1528 characterData: true
1529 });
1530 }
1531 disconnectedCallback() {
1532 this._optionObserver?.disconnect();
1533 this._optionObserver = null;
1534 }
1535 render() {
1536 const label = this.label || "";
1537 const current = this.value;
1538 const placeholder = this.placeholder || "";
1539 const disabled = this.disabled !== null;
1540 const name = this.name || "";
1541 if (label) {
1542 this.setAttribute("aria-label", label);
1543 } else {
1544 this.removeAttribute("aria-label");
1545 }
1546 const selectAriaLabel = label || placeholder;
1547 const options = this._readOptions();
1548 const hostId = this.id || "wpd-unnamed";
1549 const selectId = `${hostId}__input`;
1550 return html`
1551 ${label ? html`<label
1552 class="wpd-select__label"
1553 for=${selectId}
1554 >${label}</label>` : html``}
1555 <span class="wpd-select__wrap">
1556 <select
1557 id=${selectId}
1558 ?disabled=${disabled}
1559 aria-label=${selectAriaLabel}
1560 name=${name}
1561 @change=${(e) => this._onChange(e)}
1562 >
1563 ${placeholder && !current ? html`<option value="" disabled selected>
1564 ${placeholder}
1565 </option>` : html``}
1566 ${options.map(
1567 (o) => html`
1568 <option
1569 value=${o.value}
1570 ?disabled=${o.disabled}
1571 ?selected=${o.value === current}
1572 >
1573 ${o.label}
1574 </option>
1575 `
1576 )}
1577 </select>
1578 <!--
1579 Inline SVG — the previous dashicons-classed span
1580 never painted because the global Dashicons font
1581 stylesheet cannot cross the shadow-root boundary.
1582 An inline SVG lives inside the shadow tree, inherits
1583 currentColor via the stroke attribute, and needs
1584 no external CSS.
1585 -->
1586 <svg
1587 class="wpd-select__chevron"
1588 viewBox="0 0 12 12"
1589 width="12"
1590 height="12"
1591 aria-hidden="true"
1592 focusable="false"
1593 >
1594 <path
1595 d="M3 5l3 3 3-3"
1596 stroke="currentColor"
1597 stroke-width="1.4"
1598 stroke-linecap="round"
1599 stroke-linejoin="round"
1600 fill="none"
1601 ></path>
1602 </svg>
1603 </span>
1604 `;
1605 }
1606 _readOptions() {
1607 const out = [];
1608 const children = this.querySelectorAll(":scope > wpd-option");
1609 for (const child of Array.from(children)) {
1610 const value = child.getAttribute("value");
1611 if (value === null) {
1612 continue;
1613 }
1614 out.push({
1615 value,
1616 label: (child.textContent || value).trim(),
1617 disabled: child.hasAttribute("disabled")
1618 });
1619 }
1620 return out;
1621 }
1622 _onChange(e) {
1623 const sel = e.target;
1624 const next = sel.value;
1625 this.value = next;
1626 this.emit("wpd-pick", { value: next });
1627 }
1628 };
1629 _WpdSelect.props = [
1630 "value",
1631 "label",
1632 "placeholder",
1633 "disabled",
1634 "name"
1635 ];
1636 _WpdSelect.styles = [selectStyles];
1637 _WpdSelect.help = {
1638 title: "Select",
1639 summary: "Dropdown picker that wraps a native <select>. Mirrors the <wpd-segmented> contract (set value, listen for wpd-pick) so callers can swap tag names when a list outgrows a pill bar.",
1640 status: "stable",
1641 since: "0.11.0",
1642 props: [
1643 {
1644 name: "value",
1645 type: "string",
1646 description: "Currently selected option value."
1647 },
1648 {
1649 name: "label",
1650 type: "string",
1651 description: "Visible label rendered above the select and forwarded to the native control as aria-label."
1652 },
1653 {
1654 name: "placeholder",
1655 type: "string",
1656 description: "Disabled leading option shown when no value is set."
1657 },
1658 {
1659 name: "disabled",
1660 type: "boolean attribute",
1661 description: "Disables the native select and dims the chrome."
1662 },
1663 {
1664 name: "name",
1665 type: "string",
1666 description: "Forwarded to the native <select name=…> for form submission."
1667 }
1668 ],
1669 slots: [
1670 { name: "(default)", description: '<wpd-option value="…"> children.' }
1671 ],
1672 events: [
1673 {
1674 name: "wpd-pick",
1675 description: "Fires when the user picks a new option.",
1676 detail: "{ value: string }"
1677 }
1678 ],
1679 cssProps: [
1680 { name: "--desktop-mode-text", description: "Label + value colour." },
1681 { name: "--desktop-mode-muted", description: "Placeholder + chevron colour." }
1682 ],
1683 example: html`
1684 <wpd-select value="eur" label="Currency">
1685 <wpd-option value="eur">Euro</wpd-option>
1686 <wpd-option value="usd">US Dollar</wpd-option>
1687 <wpd-option value="jpy">Japanese Yen</wpd-option>
1688 </wpd-select>
1689 `
1690 };
1691 let WpdSelect = _WpdSelect;
1692 defineComponent("wpd-select", WpdSelect);
1693 const styles$1 = css`:host{display:block;width:100%;aspect-ratio:4 / 3}:host( [ size='small' ] ){display:inline-block;width:32px;height:32px;aspect-ratio:1 / 1;flex:0 0 auto}:host( [ variant='wallpaper' ] ){aspect-ratio:16 / 9}:host( [ variant='wallpaper' ] ) button{display:flex;align-items:flex-end;justify-content:flex-start;padding:6px 8px;overflow:hidden}button{appearance:none;width:100%;height:100%;padding:0;border-radius:10px;border:2px solid transparent;cursor:pointer;background-color:#eee;background-size:cover;background-position:center;transition:transform 0.15s ease,border-color 0.15s ease,box-shadow 0.15s ease}:host( [ size='small' ] ) button{border-radius:50%}button:hover{transform:scale( 1.04 )}button[ aria-pressed='true' ]{border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 2px var( --wp-admin-theme-color,#2271b1 )}:host( [ variant='wallpaper' ] ) button:hover{transform:translateY( -1px )}`;
1694 const _WpdSwatch = class _WpdSwatch extends Component {
1695 render() {
1696 const selected = this.selected !== null;
1697 const label = this.label || "";
1698 const preview = this.preview || "";
1699 return html`
1700 <button
1701 type="button"
1702 aria-pressed=${selected ? "true" : "false"}
1703 aria-label=${label}
1704 title=${label}
1705 style="background: ${preview}"
1706 @click=${() => this._onPick()}
1707 >
1708 <slot></slot>
1709 </button>
1710 `;
1711 }
1712 _onPick() {
1713 this.emit("wpd-pick", {
1714 value: this.value
1715 });
1716 }
1717 };
1718 _WpdSwatch.props = ["value", "label", "selected", "preview", "size", "variant"];
1719 _WpdSwatch.styles = [styles$1];
1720 _WpdSwatch.help = {
1721 title: "Swatch",
1722 summary: "Selectable color/wallpaper tile. Renders as an aria-pressed button with a background driven by the preview attribute.",
1723 status: "stable",
1724 since: "0.9.0",
1725 props: [
1726 {
1727 name: "value",
1728 type: "string",
1729 description: "Identifier emitted on the wpd-pick event when the swatch is clicked."
1730 },
1731 {
1732 name: "label",
1733 type: "string",
1734 description: "aria-label + title for the button."
1735 },
1736 {
1737 name: "selected",
1738 type: "boolean attribute",
1739 description: "Marks the swatch as the active choice within a swatch-grid."
1740 },
1741 {
1742 name: "preview",
1743 type: "CSS background value",
1744 description: "Raw CSS background (color, gradient, url()) painted on the tile."
1745 },
1746 {
1747 name: "size",
1748 type: "string",
1749 description: "Visual size hint (e.g. sm, md, lg). Consumed by the stylesheet."
1750 },
1751 {
1752 name: "variant",
1753 type: "string",
1754 description: "Optional visual variant (e.g. color vs wallpaper)."
1755 }
1756 ],
1757 slots: [
1758 { name: "(default)", description: "Optional overlay content rendered inside the tile." }
1759 ],
1760 events: [
1761 {
1762 name: "wpd-pick",
1763 description: "Fires when the swatch is clicked.",
1764 detail: "{ value: string }"
1765 }
1766 ],
1767 example: html`
1768 <wpd-swatch-grid label="Accent">
1769 <wpd-swatch value="red" preview="#ef4444" label="Red" selected></wpd-swatch>
1770 <wpd-swatch value="blue" preview="#3b82f6" label="Blue"></wpd-swatch>
1771 <wpd-swatch value="green" preview="#10b981" label="Green"></wpd-swatch>
1772 </wpd-swatch-grid>
1773 `
1774 };
1775 let WpdSwatch = _WpdSwatch;
1776 defineComponent("wpd-swatch", WpdSwatch);
1777 const styles = css`:host{display:grid;grid-template-columns:repeat( var( --wpd-swatch-grid-cols,4 ),1fr );gap:12px}:host( [ mode='row' ] ){display:flex;flex-wrap:wrap;align-items:center;gap:10px}`;
1778 const _WpdSwatchGrid = class _WpdSwatchGrid extends Component {
1779 render() {
1780 const label = this.label || "";
1781 const cols = this.columns || "";
1782 if (cols) {
1783 this.style.setProperty("--wpd-swatch-grid-cols", cols);
1784 }
1785 this.setAttribute("role", "radiogroup");
1786 if (label) {
1787 this.setAttribute("aria-label", label);
1788 }
1789 return html`<slot></slot>`;
1790 }
1791 };
1792 _WpdSwatchGrid.props = ["label", "columns", "mode"];
1793 _WpdSwatchGrid.styles = [styles];
1794 _WpdSwatchGrid.help = {
1795 title: "Swatch grid",
1796 summary: "Flex grid container for <wpd-swatch> children. Emits radiogroup semantics so screen readers announce the tiles as a unit.",
1797 status: "stable",
1798 since: "0.9.0",
1799 props: [
1800 {
1801 name: "label",
1802 type: "string",
1803 description: 'aria-label describing the group (e.g. "Accent color").'
1804 },
1805 {
1806 name: "columns",
1807 type: "CSS grid track template",
1808 description: "Overrides the default column track via --wpd-swatch-grid-cols."
1809 },
1810 {
1811 name: "mode",
1812 type: "string",
1813 description: "Optional rendering variant forwarded to child swatches."
1814 }
1815 ],
1816 slots: [
1817 { name: "(default)", description: "<wpd-swatch> children." }
1818 ],
1819 cssProps: [
1820 { name: "--wpd-swatch-grid-cols", description: "Grid column template." }
1821 ],
1822 example: html`
1823 <wpd-swatch-grid label="Wallpaper">
1824 <wpd-swatch value="a" preview="linear-gradient(135deg,#f093fb,#f5576c)"></wpd-swatch>
1825 <wpd-swatch value="b" preview="linear-gradient(135deg,#4facfe,#00f2fe)"></wpd-swatch>
1826 <wpd-swatch value="c" preview="linear-gradient(135deg,#43e97b,#38f9d7)"></wpd-swatch>
1827 </wpd-swatch-grid>
1828 `
1829 };
1830 let WpdSwatchGrid = _WpdSwatchGrid;
1831 defineComponent("wpd-swatch-grid", WpdSwatchGrid);
1832 const tabsStyles = css`:host{display:flex;gap:4px;margin-bottom:10px;border-bottom:1px solid var( --desktop-mode-border,#dcdcde )}`;
1833 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}`;
1834 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 )}`;
1835 const _WpdTab = class _WpdTab extends Component {
1836 render() {
1837 this.setAttribute("role", "tab");
1838 return html`
1839 <button type="button" @click=${() => this._onPick()}>
1840 <slot></slot>
1841 </button>
1842 `;
1843 }
1844 _onPick() {
1845 this.emit("wpd-tab-pick", {
1846 value: this.value
1847 });
1848 }
1849 };
1850 _WpdTab.props = ["value"];
1851 _WpdTab.styles = [tabStyles];
1852 _WpdTab.help = {
1853 title: "Tab",
1854 summary: "Single tab inside a <wpd-tabs> strip. Carries its identifier via `value`; aria-selected + tabindex are mirrored by the parent.",
1855 status: "stable",
1856 since: "0.7.0",
1857 props: [
1858 {
1859 name: "value",
1860 type: "string",
1861 description: "Identifier the tab contributes to the parent strip selection."
1862 }
1863 ],
1864 slots: [
1865 { name: "(default)", description: "Visible tab label." }
1866 ],
1867 events: [
1868 {
1869 name: "wpd-tab-pick",
1870 description: "Internal event bubbled to the parent <wpd-tabs>. Consumers should listen for wpd-tab-change on the strip instead.",
1871 detail: "{ value: string | null }"
1872 }
1873 ]
1874 };
1875 let WpdTab = _WpdTab;
1876 defineComponent("wpd-tab", WpdTab);
1877 const _WpdTabs = class _WpdTabs extends Component {
1878 connectedCallback() {
1879 super.connectedCallback();
1880 this.addEventListener("wpd-tab-pick", (e) => {
1881 const detail = e.detail;
1882 e.stopPropagation();
1883 this.value = detail.value;
1884 this.emit("wpd-tab-change", { value: detail.value });
1885 });
1886 }
1887 /**
1888 * Declarative item-list setter. Replaces the existing `<wpd-tab>`
1889 * children with a fresh set built from a `{ value, label }`
1890 * array. The `value` prop is preserved if it still matches a new
1891 * entry; otherwise it falls back to the first item.
1892 *
1893 * Lets plugins that populate tabs dynamically (route-driven
1894 * admin screens, filtered lists) replace the declarative
1895 * markup with a one-liner:
1896 *
1897 * ```js
1898 * tabs.items = [
1899 * { value: 'calc', label: 'Calc' },
1900 * { value: 'convert', label: 'Convert' },
1901 * ];
1902 * ```
1903 *
1904 * @since 0.11.0
1905 */
1906 set items(list2) {
1907 replaceChildren(this, "wpd-tab", list2);
1908 const current = this.value;
1909 const stillValid = current !== null && list2.some((i) => i.value === current);
1910 if (!stillValid && list2.length > 0) {
1911 this.value = list2[0].value;
1912 } else {
1913 this.requestUpdate();
1914 }
1915 }
1916 render() {
1917 this.setAttribute("role", "tablist");
1918 const label = this.label || "";
1919 if (label) {
1920 this.setAttribute("aria-label", label);
1921 }
1922 const current = this.value;
1923 queueMicrotask(() => {
1924 const tabs = this.querySelectorAll("wpd-tab");
1925 for (const tab of Array.from(tabs)) {
1926 const v = tab.getAttribute("value");
1927 tab.setAttribute(
1928 "aria-selected",
1929 v === current ? "true" : "false"
1930 );
1931 tab.setAttribute("tabindex", v === current ? "0" : "-1");
1932 }
1933 syncTabpanels(this, current);
1934 });
1935 return html`<slot></slot>`;
1936 }
1937 };
1938 _WpdTabs.props = ["value", "label"];
1939 _WpdTabs.styles = [tabsStyles];
1940 _WpdTabs.help = {
1941 title: "Tabs",
1942 summary: 'Underline-accent tab strip. Pair with sibling <wpd-tabpanel for="…"> elements and the strip auto-toggles their hidden attribute on selection.',
1943 status: "stable",
1944 since: "0.7.0",
1945 props: [
1946 {
1947 name: "value",
1948 type: "string",
1949 description: "Currently active tab value. Mirrored to child <wpd-tab> aria-selected."
1950 },
1951 {
1952 name: "label",
1953 type: "string",
1954 description: "aria-label for the tablist — describe the tab group for assistive tech."
1955 }
1956 ],
1957 slots: [
1958 {
1959 name: "(default)",
1960 description: '<wpd-tab value="…"> children forming the strip.'
1961 }
1962 ],
1963 events: [
1964 {
1965 name: "wpd-tab-change",
1966 description: "Fires when the active tab changes.",
1967 detail: "{ value: string }"
1968 }
1969 ],
1970 example: html`
1971 <wpd-tabs value="one" label="Demo tabs">
1972 <wpd-tab value="one">One</wpd-tab>
1973 <wpd-tab value="two">Two</wpd-tab>
1974 <wpd-tab value="three">Three</wpd-tab>
1975 </wpd-tabs>
1976 <wpd-tabpanel for="one">First panel.</wpd-tabpanel>
1977 <wpd-tabpanel for="two">Second panel.</wpd-tabpanel>
1978 <wpd-tabpanel for="three">Third panel.</wpd-tabpanel>
1979 `
1980 };
1981 let WpdTabs = _WpdTabs;
1982 defineComponent("wpd-tabs", WpdTabs);
1983 const _WpdTabPanel = class _WpdTabPanel extends Component {
1984 // Shadow DOM — the render target for this component is its
1985 // own shadow root, which holds a single `<slot>` that projects
1986 // whatever the caller placed between the `<wpd-tabpanel>` open
1987 // and close tags. Slotted children remain light-DOM descendants
1988 // of the panel element (the slot rendering mechanism doesn't
1989 // move them), so `panel.querySelector(...)` from plugin render
1990 // callbacks keeps working.
1991 //
1992 // Earlier 0.11.0 builds of this component used light DOM with
1993 // a `<slot>` render, which wiped the panel's server-rendered
1994 // template content on first mount — every `render()` writes
1995 // into `_renderRoot`, and with light DOM that's the panel
1996 // itself. Shadow DOM isolates the render surface.
1997 connectedCallback() {
1998 super.connectedCallback();
1999 this.setAttribute("role", "tabpanel");
2000 if (!this.hasAttribute("tabindex")) {
2001 this.setAttribute("tabindex", "0");
2002 }
2003 const owner = findOwningTabs(this);
2004 if (owner) {
2005 syncTabpanels(owner, owner.getAttribute("value"));
2006 }
2007 }
2008 render() {
2009 return html`<slot></slot>`;
2010 }
2011 };
2012 _WpdTabPanel.props = ["for"];
2013 _WpdTabPanel.styles = [tabPanelStyles];
2014 _WpdTabPanel.help = {
2015 title: "Tab panel",
2016 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.',
2017 status: "stable",
2018 since: "0.11.0",
2019 props: [
2020 {
2021 name: "for",
2022 type: "string",
2023 description: "Matches the `value` of the owning <wpd-tab>. Panel is shown when its parent tabs strip is on that value."
2024 }
2025 ],
2026 slots: [
2027 { name: "(default)", description: "Panel body content." }
2028 ]
2029 };
2030 let WpdTabPanel = _WpdTabPanel;
2031 defineComponent("wpd-tabpanel", WpdTabPanel);
2032 function replaceChildren(host, tag, items) {
2033 const existing = host.querySelectorAll(`:scope > ${tag}`);
2034 for (const el of Array.from(existing)) {
2035 el.remove();
2036 }
2037 for (const item of items) {
2038 const el = document.createElement(tag);
2039 el.setAttribute("value", item.value);
2040 el.textContent = item.label;
2041 host.appendChild(el);
2042 }
2043 }
2044 function findOwningTabs(panel) {
2045 const parent = panel.parentElement;
2046 if (!parent) {
2047 return null;
2048 }
2049 const sibling = parent.querySelector(":scope > wpd-tabs");
2050 if (sibling) {
2051 return sibling;
2052 }
2053 return panel.closest("wpd-tabs");
2054 }
2055 function syncTabpanels(tabs, value) {
2056 const panels = /* @__PURE__ */ new Set();
2057 const parent = tabs.parentElement;
2058 if (parent) {
2059 for (const p of Array.from(
2060 parent.querySelectorAll(":scope > wpd-tabpanel")
2061 )) {
2062 panels.add(p);
2063 }
2064 }
2065 for (const p of Array.from(
2066 tabs.querySelectorAll(":scope > wpd-tabpanel")
2067 )) {
2068 panels.add(p);
2069 }
2070 for (const panel of panels) {
2071 const pfor = panel.getAttribute("for");
2072 const active = pfor !== null && pfor === value;
2073 if (active) {
2074 panel.removeAttribute("hidden");
2075 } else {
2076 panel.setAttribute("hidden", "");
2077 }
2078 panel.setAttribute("aria-hidden", active ? "false" : "true");
2079 }
2080 }
2081 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}`;
2082 const _WpdTextField = class _WpdTextField extends Component {
2083 constructor() {
2084 super(...arguments);
2085 this._revealed = false;
2086 }
2087 connectedCallback() {
2088 super.connectedCallback();
2089 ensureAutoId(this);
2090 }
2091 render() {
2092 const label = this.label || "";
2093 const value = this.value ?? "";
2094 const placeholder = this.placeholder || "";
2095 const disabled = this.disabled !== null;
2096 const readonly = this.readonly !== null;
2097 const declaredAutocomplete = this.autocomplete;
2098 const declaredType = this.type || "text";
2099 const isPassword = declaredType === "password";
2100 let autocomplete = declaredAutocomplete || "off";
2101 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
2102 autocomplete = "new-password";
2103 }
2104 const maxLength = this.maxlength;
2105 const minLength = this.minlength;
2106 const pattern = this.pattern || "";
2107 const name = this.name || "";
2108 const suffix = this.suffix || "";
2109 const invalid = this.invalid !== null;
2110 const reveal = this.reveal !== null;
2111 const isPasswordIntent = declaredType === "password";
2112 const isMasked = isPasswordIntent && !(reveal && this._revealed);
2113 let effectiveType;
2114 if (isPasswordIntent) {
2115 effectiveType = "text";
2116 } else if (reveal && this._revealed) {
2117 effectiveType = "text";
2118 } else {
2119 effectiveType = declaredType;
2120 }
2121 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
2122 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
2123 const hostId = this.id || "wpd-unnamed";
2124 const inputId = `${hostId}__input`;
2125 return html`
2126 ${label ? html`<label
2127 class="wpd-text-field__label"
2128 for=${inputId}
2129 >${label}</label>` : html``}
2130 <span class=${rowClass}>
2131 <input
2132 id=${inputId}
2133 class=${inputClass}
2134 type=${effectiveType}
2135 .value=${value}
2136 placeholder=${placeholder}
2137 ?disabled=${disabled}
2138 ?readonly=${readonly}
2139 autocomplete=${autocomplete}
2140 maxlength=${maxLength ?? ""}
2141 minlength=${minLength ?? ""}
2142 pattern=${pattern}
2143 name=${name}
2144 aria-invalid=${invalid ? "true" : "false"}
2145 aria-label=${label || ""}
2146 @input=${(e) => this._onInput(e)}
2147 @change=${(e) => this._onChange(e)}
2148 @keydown=${(e) => this._onKeyDown(e)}
2149 />
2150 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
2151 ${reveal ? this._renderRevealButton(disabled) : html``}
2152 </span>
2153 `;
2154 }
2155 _renderRevealButton(disabled) {
2156 const label = this._revealed ? "Hide" : "Show";
2157 return html`
2158 <button
2159 type="button"
2160 class="wpd-text-field__reveal"
2161 aria-label=${label}
2162 aria-pressed=${this._revealed ? "true" : "false"}
2163 ?disabled=${disabled}
2164 tabindex="0"
2165 @click=${() => this._onToggleReveal()}
2166 >
2167 ${this._revealed ? _iconEyeOff() : _iconEye()}
2168 </button>
2169 `;
2170 }
2171 _onToggleReveal() {
2172 this._revealed = !this._revealed;
2173 this.requestUpdate();
2174 }
2175 _onInput(e) {
2176 const input = e.target;
2177 this.value = input.value;
2178 this.emit("wpd-input-change", { value: input.value });
2179 }
2180 _onChange(e) {
2181 const input = e.target;
2182 this.emit("wpd-input-commit", { value: input.value });
2183 }
2184 _onKeyDown(e) {
2185 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
2186 const input = e.target;
2187 this.emit("wpd-submit", { value: input.value });
2188 }
2189 }
2190 };
2191 _WpdTextField.props = [
2192 "label",
2193 "value",
2194 "placeholder",
2195 "disabled",
2196 "readonly",
2197 "autocomplete",
2198 "type",
2199 "maxlength",
2200 "minlength",
2201 "pattern",
2202 "name",
2203 "suffix",
2204 "invalid",
2205 "reveal"
2206 ];
2207 _WpdTextField.styles = [textFieldStyles];
2208 _WpdTextField.help = {
2209 title: "Text field",
2210 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.",
2211 status: "stable",
2212 since: "0.11.0",
2213 props: [
2214 { name: "label", type: "string", description: "Visible label above the input." },
2215 { name: "value", type: "string", description: "Current input value; reflected two-way." },
2216 { name: "placeholder", type: "string", description: "Native placeholder string." },
2217 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
2218 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
2219 {
2220 name: "autocomplete",
2221 type: "string",
2222 default: "off",
2223 description: "Forwarded to the native input autocomplete attribute."
2224 },
2225 {
2226 name: "type",
2227 type: "string",
2228 default: "text",
2229 description: "Native input type (text, password, email, search, tel, url)."
2230 },
2231 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
2232 { name: "minlength", type: "integer (string)", description: "Native minlength." },
2233 { name: "pattern", type: "regex string", description: "Native validation pattern." },
2234 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
2235 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
2236 {
2237 name: "invalid",
2238 type: "boolean attribute",
2239 description: "Marks the field aria-invalid and applies the error style."
2240 },
2241 {
2242 name: "reveal",
2243 type: "boolean attribute",
2244 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
2245 }
2246 ],
2247 events: [
2248 {
2249 name: "wpd-input-change",
2250 description: "Fires on every input keystroke.",
2251 detail: "{ value: string }"
2252 },
2253 {
2254 name: "wpd-input-commit",
2255 description: "Fires on the native change event (blur / Enter).",
2256 detail: "{ value: string }"
2257 },
2258 {
2259 name: "wpd-submit",
2260 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
2261 detail: "{ value: string }"
2262 }
2263 ],
2264 cssProps: [
2265 { name: "--desktop-mode-text", description: "Text colour." },
2266 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
2267 { name: "--desktop-mode-border", description: "Input outline." },
2268 { name: "--desktop-mode-window-bg", description: "Input background." }
2269 ],
2270 example: html`
2271 <wpd-stack gap="8">
2272 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
2273 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
2274 </wpd-stack>
2275 `
2276 };
2277 let WpdTextField = _WpdTextField;
2278 defineComponent("wpd-text-field", WpdTextField);
2279 function _iconEye() {
2280 return html`
2281 <svg
2282 viewBox="0 0 16 16"
2283 width="14"
2284 height="14"
2285 fill="none"
2286 stroke="currentColor"
2287 stroke-width="1.5"
2288 stroke-linecap="round"
2289 stroke-linejoin="round"
2290 aria-hidden="true"
2291 focusable="false"
2292 >
2293 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
2294 <circle cx="8" cy="8" r="2" />
2295 </svg>
2296 `;
2297 }
2298 function _iconEyeOff() {
2299 return html`
2300 <svg
2301 viewBox="0 0 16 16"
2302 width="14"
2303 height="14"
2304 fill="none"
2305 stroke="currentColor"
2306 stroke-width="1.5"
2307 stroke-linecap="round"
2308 stroke-linejoin="round"
2309 aria-hidden="true"
2310 focusable="false"
2311 >
2312 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
2313 <circle cx="8" cy="8" r="2" />
2314 <line x1="2" y1="2" x2="14" y2="14" />
2315 </svg>
2316 `;
2317 }
2318 const HD_MIN_WIDTH = 1920;
2319 const HD_MIN_HEIGHT = 1080;
2320 const MEDIA_PER_PAGE = 40;
2321 const SEARCH_DEBOUNCE_MS = 300;
2322 const CUSTOM_GRADIENT_ID = "custom-gradient";
2323 const CUSTOM_IMAGE_ID = "custom-image";
2324 const DEFAULT_WALLPAPER_ID = "dark";
2325 const DEFAULT_ACCENTS = [
2326 { id: "wp-blue", label: "WordPress Blue", value: "#2271b1" },
2327 { id: "indigo", label: "Indigo", value: "#3858e9" },
2328 { id: "teal", label: "Teal", value: "#04a4cc" },
2329 { id: "emerald", label: "Emerald", value: "#059669" },
2330 { id: "amber", label: "Amber", value: "#d97706" },
2331 { id: "rose", label: "Rose", value: "#e11d48" }
2332 ];
2333 function getAccents() {
2334 const config = window.wp?.desktop?.config;
2335 const raw = config?.accentColors;
2336 if (!Array.isArray(raw) || raw.length === 0) {
2337 return DEFAULT_ACCENTS;
2338 }
2339 const clean = [];
2340 for (const entry of raw) {
2341 if (entry && typeof entry === "object" && typeof entry.id === "string" && typeof entry.label === "string" && typeof entry.value === "string" && entry.id !== "" && entry.label !== "" && /^#[0-9a-f]{3,8}$/i.test(entry.value)) {
2342 clean.push({ id: entry.id, label: entry.label, value: entry.value });
2343 }
2344 }
2345 return clean.length > 0 ? clean : DEFAULT_ACCENTS;
2346 }
2347 const DOCK_SIZES = [
2348 { id: "compact", label: "Compact", width: 48, icon: 18 },
2349 { id: "default", label: "Default", width: 56, icon: 20 },
2350 { id: "large", label: "Large", width: 72, icon: 26 }
2351 ];
2352 const DESKTOP_LAYOUTS = [
2353 { id: "classic", label: "Classic" },
2354 { id: "unified", label: "Unified" },
2355 { id: "spatial", label: "Spatial" }
2356 ];
2357 const DEFAULTS = {
2358 wallpaper: DEFAULT_WALLPAPER_ID,
2359 accent: "wp-blue",
2360 dockSize: "default",
2361 desktopLayout: "classic",
2362 dockRailRenderer: "default",
2363 customGradient: {
2364 from: "#2271b1",
2365 to: "#7c3aed",
2366 angle: 135
2367 },
2368 customImage: null,
2369 libraryHdOnly: true,
2370 ai: {
2371 enabled: false,
2372 provider: "openai",
2373 apiKey: "",
2374 apiKeys: {},
2375 transport: "off"
2376 },
2377 // Opt-out as of 0.8.0. Fresh installs land on the native Posts
2378 // window — same screen the rest of desktop mode is built for. A
2379 // user can still flip this off to fall back to the chromeless
2380 // `edit.php` iframe, but the new default is "use the native UI."
2381 heartbeatRate: 60,
2382 nativePostsEnabled: true,
2383 nativePostsHiddenColumns: [],
2384 // Same opt-out posture as Posts — fresh installs land on the
2385 // native Pages window, users can flip back to the iframe.
2386 nativePagesEnabled: true,
2387 // Native Users window — same opt-out posture. Capability-gated
2388 // server-side (the window is only registered for users with
2389 // `list_users`), so flipping this off only affects the small set
2390 // of users who can see the Users tile in the first place.
2391 nativeUsersEnabled: true,
2392 // Native Plugins window — replaces `plugins.php` and
2393 // `plugin-install.php`. Same opt-out posture; cap-gated on
2394 // `activate_plugins` server-side, so flipping this off only
2395 // affects users who could see the Plugins tile anyway.
2396 nativePluginsEnabled: true,
2397 // Native Comments window — replaces `edit-comments.php`. Same
2398 // opt-out posture; cap-gated on `edit_posts` server-side.
2399 nativeCommentsEnabled: true,
2400 showDesktopOnWallpaperClick: false,
2401 showPostStatusRibbons: true,
2402 foldersSharingEnabled: true,
2403 itemVisibility: {},
2404 dockOrder: [],
2405 dockPromotedPositions: {}
2406 };
2407 const AI_TRANSPORTS = [
2408 { id: "off", label: "Off" },
2409 { id: "sse", label: "Streaming (SSE)" }
2410 ];
2411 const AI_PROVIDERS = [
2412 {
2413 id: "openai",
2414 label: "OpenAI",
2415 apiKeyLabel: "OpenAI API key",
2416 apiKeyLink: "https://platform.openai.com/api-keys"
2417 }
2418 ];
2419 function getAiProviders() {
2420 const cfg = window.desktopModeConfig;
2421 const list2 = cfg?.aiProviders;
2422 if (!Array.isArray(list2) || list2.length === 0) {
2423 return AI_PROVIDERS;
2424 }
2425 return list2.map((p) => ({
2426 id: p.id,
2427 label: p.label,
2428 description: p.description,
2429 apiKeyLabel: p.api_key_label,
2430 apiKeyLink: p.api_key_link
2431 }));
2432 }
2433 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
2434 function resolveSlot() {
2435 const w = window;
2436 let slot = w[SHARED_STORES_SLOT];
2437 if (!slot) {
2438 slot = /* @__PURE__ */ new Map();
2439 w[SHARED_STORES_SLOT] = slot;
2440 }
2441 return slot;
2442 }
2443 function createSharedStore(key, initialState) {
2444 const slot = resolveSlot();
2445 let record = slot.get(key);
2446 if (!record) {
2447 record = {
2448 state: initialState(),
2449 listeners: /* @__PURE__ */ new Set(),
2450 rebuild: initialState
2451 };
2452 slot.set(key, record);
2453 }
2454 const handle = {
2455 // `record.state` is the live reference. The getter on the
2456 // `state` field reads the latest value even if `reset()`
2457 // reassigned it to a fresh object.
2458 get state() {
2459 return record.state;
2460 },
2461 set state(next) {
2462 record.state = next;
2463 },
2464 getState() {
2465 return record.state;
2466 },
2467 notify() {
2468 for (const cb of Array.from(record.listeners)) {
2469 try {
2470 cb(record.state);
2471 } catch (err) {
2472 console.error(
2473 `[desktop-mode/shared-store:${key}] subscriber threw:`,
2474 err
2475 );
2476 }
2477 }
2478 },
2479 subscribe(cb) {
2480 record.listeners.add(cb);
2481 return () => {
2482 record.listeners.delete(cb);
2483 };
2484 },
2485 setState(patch) {
2486 const cur = record.state;
2487 if (typeof cur !== "object" || cur === null) {
2488 console.warn(
2489 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
2490 );
2491 return;
2492 }
2493 Object.assign(cur, patch);
2494 handle.notify();
2495 },
2496 reset() {
2497 const fresh = record.rebuild();
2498 const cur = record.state;
2499 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
2500 const target = cur;
2501 for (const k of Object.keys(target)) {
2502 delete target[k];
2503 }
2504 Object.assign(target, fresh);
2505 } else {
2506 record.state = fresh;
2507 }
2508 record.listeners.clear();
2509 }
2510 };
2511 return handle;
2512 }
2513 const store$2 = createSharedStore(
2514 "desktop-mode/settings-tab-registry",
2515 () => ({
2516 registry: /* @__PURE__ */ new Map(),
2517 listeners: /* @__PURE__ */ new Set()
2518 })
2519 );
2520 const registry$1 = store$2.state.registry;
2521 const listeners$2 = store$2.state.listeners;
2522 function listSettingsTabs() {
2523 return Array.from(registry$1.values()).sort(
2524 (a, b) => (a.order ?? 100) - (b.order ?? 100)
2525 );
2526 }
2527 function subscribeSettingsTabs(cb) {
2528 listeners$2.add(cb);
2529 return () => {
2530 listeners$2.delete(cb);
2531 };
2532 }
2533 let loadPromise = null;
2534 function loadImpl(scriptUrl) {
2535 if (window.desktopModeMountAboutScene) {
2536 return Promise.resolve(window.desktopModeMountAboutScene);
2537 }
2538 if (loadPromise) {
2539 return loadPromise;
2540 }
2541 loadPromise = new Promise((resolve, reject) => {
2542 const existing = document.querySelector(
2543 'script[data-desktop-mode-about-scene="1"]'
2544 );
2545 const finish = () => {
2546 const fn = window.desktopModeMountAboutScene;
2547 if (!fn) {
2548 reject(
2549 new Error(
2550 "[desktop-mode] about-scene bundle loaded but did not register desktopModeMountAboutScene"
2551 )
2552 );
2553 return;
2554 }
2555 resolve(fn);
2556 };
2557 if (existing) {
2558 if (window.desktopModeMountAboutScene) {
2559 finish();
2560 } else {
2561 existing.addEventListener("load", finish);
2562 existing.addEventListener(
2563 "error",
2564 () => reject(new Error("failed to load about-scene bundle"))
2565 );
2566 }
2567 return;
2568 }
2569 const s = document.createElement("script");
2570 s.src = scriptUrl;
2571 s.async = true;
2572 s.dataset.desktopModeAboutScene = "1";
2573 s.addEventListener("load", finish);
2574 s.addEventListener(
2575 "error",
2576 () => reject(new Error("failed to load about-scene bundle"))
2577 );
2578 document.head.appendChild(s);
2579 });
2580 return loadPromise;
2581 }
2582 async function mountAboutSceneLazy(opts, scriptUrl) {
2583 const fn = await loadImpl(scriptUrl);
2584 return fn(opts);
2585 }
2586 function waitForSize(el) {
2587 if (el.clientWidth > 0 && el.clientHeight > 0) {
2588 return Promise.resolve();
2589 }
2590 return new Promise((resolve) => {
2591 const observer = new ResizeObserver(() => {
2592 if (el.clientWidth > 0 && el.clientHeight > 0) {
2593 observer.disconnect();
2594 resolve();
2595 }
2596 });
2597 observer.observe(el);
2598 });
2599 }
2600 function buildAboutSection() {
2601 const wrapper = document.createElement("div");
2602 wrapper.classList.add("desktop-mode-os-settings__about");
2603 const config = window.desktopModeConfig ?? {};
2604 const pluginUrl = config.pluginUrl ?? "";
2605 const version = config.pluginVersion ?? "";
2606 const aboutSceneBundleUrl = config.aboutSceneBundleUrl ?? "";
2607 const desktopApi = window.wp?.desktop;
2608 render(
2609 html`
2610 <div
2611 class="desktop-mode-os-settings__about-stage-host"
2612 data-about-stage
2613 ></div>
2614 `,
2615 wrapper
2616 );
2617 let scene = null;
2618 let aborted = false;
2619 const tearDown = () => {
2620 aborted = true;
2621 if (scene) {
2622 try {
2623 scene.destroy();
2624 } catch {
2625 }
2626 scene = null;
2627 }
2628 };
2629 const mount = async () => {
2630 if (aborted || !wrapper.isConnected) {
2631 return;
2632 }
2633 const host = wrapper.querySelector("[data-about-stage]");
2634 if (!host) {
2635 return;
2636 }
2637 try {
2638 if (desktopApi?.loadModules) {
2639 await desktopApi.loadModules(["pixijs"]);
2640 }
2641 if (aborted || !wrapper.isConnected) {
2642 return;
2643 }
2644 await waitForSize(host);
2645 if (aborted || !wrapper.isConnected) {
2646 return;
2647 }
2648 const built = await mountAboutSceneLazy(
2649 {
2650 container: host,
2651 logoUrl: `${pluginUrl}/assets/images/automattic-logotype-color.png`,
2652 prefersReducedMotion: typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches,
2653 labels: {
2654 eyebrow: __("WordPress Desktop Mode"),
2655 title: __("Crafted with curiosity"),
2656 byline: __("an experiment by Automattic"),
2657 version: version ? `${__("Version")} ${version}` : "",
2658 hint: __("Move your cursor through the swarm · click for a spark")
2659 }
2660 },
2661 aboutSceneBundleUrl
2662 );
2663 if (aborted || !wrapper.isConnected) {
2664 built.destroy();
2665 return;
2666 }
2667 scene = built;
2668 } catch (err) {
2669 if (typeof console !== "undefined") {
2670 console.error("[desktop-mode/about] scene mount failed:", err);
2671 }
2672 }
2673 };
2674 requestAnimationFrame(() => {
2675 void mount();
2676 });
2677 const observer = new MutationObserver(() => {
2678 if (!wrapper.isConnected) {
2679 tearDown();
2680 observer.disconnect();
2681 }
2682 });
2683 observer.observe(document.body, { childList: true, subtree: true });
2684 return wrapper;
2685 }
2686 function translateAccentLabel(id, fallback) {
2687 switch (id) {
2688 case "wp-blue":
2689 return __("WordPress Blue");
2690 case "indigo":
2691 return __("Indigo");
2692 case "teal":
2693 return __("Teal");
2694 case "emerald":
2695 return __("Emerald");
2696 case "amber":
2697 return __("Amber");
2698 case "rose":
2699 return __("Rose");
2700 default:
2701 return fallback;
2702 }
2703 }
2704 function translateDockSizeLabel(id, fallback) {
2705 switch (id) {
2706 case "compact":
2707 return __("Compact");
2708 case "default":
2709 return __("Default");
2710 case "large":
2711 return __("Large");
2712 default:
2713 return fallback;
2714 }
2715 }
2716 function translateDesktopLayoutLabel(id, fallback) {
2717 switch (id) {
2718 case "classic":
2719 return __("Classic");
2720 case "unified":
2721 return __("Unified");
2722 case "spatial":
2723 return __("Spatial");
2724 default:
2725 return fallback;
2726 }
2727 }
2728 function translateDesktopLayoutDescription(id) {
2729 switch (id) {
2730 case "classic":
2731 return __(
2732 "Side bar with the core admin menus, plus a bottom dock for plugin apps."
2733 );
2734 case "unified":
2735 return __(
2736 "Single bottom dock holding every menu — core and plugin apps share one rail."
2737 );
2738 case "spatial":
2739 return __(
2740 "Bottom dock for plugin apps; core admin menus appear as icons on the wallpaper."
2741 );
2742 default:
2743 return "";
2744 }
2745 }
2746 function buildAccentSection(ctx) {
2747 const onPick = (e) => {
2748 const id = e.detail?.value ?? "";
2749 if (!getAccents().some((a) => a.id === id)) {
2750 return;
2751 }
2752 ctx.state.accent = id;
2753 ctx.save();
2754 ctx.apply();
2755 paint();
2756 };
2757 const wrapper = document.createElement("div");
2758 const paint = () => render(
2759 html`
2760 <wpd-section
2761 heading=${__("Accent color")}
2762 description=${__("Used in focused window title bars, buttons, and focus rings.")}
2763 >
2764 <wpd-swatch-grid
2765 label=${__("Accent color")}
2766 mode="row"
2767 @wpd-pick=${onPick}
2768 >
2769 ${getAccents().map(
2770 (a) => html`<wpd-swatch
2771 value=${a.id}
2772 label=${translateAccentLabel(a.id, a.label)}
2773 preview=${a.value}
2774 size="small"
2775 ?selected=${ctx.state.accent === a.id}
2776 ></wpd-swatch>`
2777 )}
2778 </wpd-swatch-grid>
2779 </wpd-section>
2780 `,
2781 wrapper
2782 );
2783 paint();
2784 return wrapper;
2785 }
2786 const NONCE_HEADER = "X-WP-Nonce";
2787 function injectRestNonce(input, init) {
2788 const nonce = readRestNonce();
2789 if (!nonce) {
2790 return init;
2791 }
2792 const url = resolveUrl(input);
2793 if (!url || !isSameOriginRestUrl(url)) {
2794 return init;
2795 }
2796 const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
2797 const headers = new Headers(baseHeaders ?? {});
2798 if (headers.has(NONCE_HEADER)) {
2799 return init;
2800 }
2801 headers.set(NONCE_HEADER, nonce);
2802 return { ...init ?? {}, headers };
2803 }
2804 function readRestNonce() {
2805 if (typeof window === "undefined") {
2806 return void 0;
2807 }
2808 const cfg = window.desktopModeConfig;
2809 const value = cfg?.restNonce;
2810 return typeof value === "string" && value.length > 0 ? value : void 0;
2811 }
2812 function resolveUrl(input) {
2813 try {
2814 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
2815 if (typeof input === "string") {
2816 return new URL(input, base);
2817 }
2818 if (input instanceof URL) {
2819 return input;
2820 }
2821 if (typeof Request !== "undefined" && input instanceof Request) {
2822 return new URL(input.url, base);
2823 }
2824 return null;
2825 } catch {
2826 return null;
2827 }
2828 }
2829 function isSameOriginRestUrl(url) {
2830 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
2831 return false;
2832 }
2833 if (url.pathname.includes("/wp-json/")) {
2834 return true;
2835 }
2836 if (url.searchParams.has("rest_route")) {
2837 return true;
2838 }
2839 return false;
2840 }
2841 function trackedFetch(input, init, opts = {}) {
2842 const fn = window.wp?.desktop?.fetch;
2843 if (typeof fn === "function") {
2844 return fn(input, init, opts);
2845 }
2846 const finalInit = injectRestNonce(input, init);
2847 return fetch(input, finalInit);
2848 }
2849 function buildAiSection(ctx) {
2850 const wrapper = document.createElement("div");
2851 const onToggle = (e) => {
2852 const checked = e.detail?.checked === true;
2853 ctx.state.ai = { ...ctx.state.ai, enabled: checked };
2854 ctx.save();
2855 paint();
2856 };
2857 const onProvider = (e) => {
2858 const id = e.detail?.value ?? "";
2859 if (!getAiProviders().some((p) => p.id === id)) {
2860 return;
2861 }
2862 const prev = ctx.state.ai.provider;
2863 const apiKeys = { ...ctx.state.ai.apiKeys ?? {} };
2864 if (ctx.state.ai.apiKey) {
2865 apiKeys[prev] = ctx.state.ai.apiKey;
2866 }
2867 ctx.state.ai = {
2868 ...ctx.state.ai,
2869 provider: id,
2870 apiKeys,
2871 apiKey: apiKeys[id] ?? ""
2872 };
2873 ctx.save();
2874 paint();
2875 };
2876 const onApiKey = (e) => {
2877 const value = e.detail?.value ?? "";
2878 const apiKeys = { ...ctx.state.ai.apiKeys ?? {} };
2879 apiKeys[ctx.state.ai.provider] = value;
2880 ctx.state.ai = { ...ctx.state.ai, apiKey: value, apiKeys };
2881 ctx.save();
2882 };
2883 const onTransport = (e) => {
2884 const id = e.detail?.value ?? "";
2885 if (!AI_TRANSPORTS.some((t) => t.id === id)) {
2886 return;
2887 }
2888 ctx.state.ai = { ...ctx.state.ai, transport: id };
2889 ctx.save();
2890 };
2891 const paint = () => {
2892 const platformEnabled = ctx.config.aiPlatformSettings?.enabled === true && !!ctx.config.aiPlatformSettings?.apiKey;
2893 const activeProvider = getAiProviders().find((p) => p.id === ctx.state.ai.provider) ?? getAiProviders()[0];
2894 const apiKeyLabel = activeProvider?.apiKeyLabel ?? __("API key");
2895 render(
2896 html`
2897 <wpd-section
2898 heading=${__("AI integration")}
2899 description=${platformEnabled ? __("A platform-wide AI key is configured. You can optionally set a personal key below to override it.") : __("Connect an AI provider to power assistive features across the desktop.")}
2900 >
2901 <wpd-checkbox-label
2902 label=${__("Enable AI features")}
2903 ?checked=${ctx.state.ai.enabled}
2904 @wpd-checkbox-change=${onToggle}
2905 ></wpd-checkbox-label>
2906
2907 <wpd-select
2908 label=${__("Provider")}
2909 value=${ctx.state.ai.provider}
2910 ?disabled=${!ctx.state.ai.enabled}
2911 @wpd-pick=${onProvider}
2912 >
2913 ${getAiProviders().map(
2914 (p) => html`<wpd-option value=${p.id}>${p.label}</wpd-option>`
2915 )}
2916 </wpd-select>
2917
2918 <wpd-text-field
2919 label=${apiKeyLabel}
2920 type="password"
2921 reveal
2922 autocomplete="off"
2923 placeholder=${platformEnabled ? __("Using platform key — enter to override") : __("sk-…")}
2924 value=${ctx.state.ai.apiKey}
2925 ?disabled=${!ctx.state.ai.enabled}
2926 @wpd-input-change=${onApiKey}
2927 ></wpd-text-field>
2928
2929 <wpd-select
2930 label=${__("Live progress updates")}
2931 value=${ctx.state.ai.transport}
2932 ?disabled=${!ctx.state.ai.enabled}
2933 @wpd-pick=${onTransport}
2934 >
2935 ${AI_TRANSPORTS.map(
2936 (t) => html`<wpd-option value=${t.id}>${t.label}</wpd-option>`
2937 )}
2938 </wpd-select>
2939 <p class="desktop-mode-ext__hint">
2940 ${__('How the assistant streams progress while it works. Pick Off if your host blocks long-lived connections (e.g. you see "Lost connection to the assistant" errors).')}
2941 </p>
2942 </wpd-section>
2943
2944 ${ctx.config.isAdmin ? _buildGlobalSection(ctx) : html``}
2945 `,
2946 wrapper
2947 );
2948 };
2949 paint();
2950 return wrapper;
2951 }
2952 function _buildGlobalSection(ctx) {
2953 const { aiPlatformSettingsUrl: url, restNonce: nonce, aiPlatformSettings: initial } = ctx.config;
2954 const state = {
2955 enabled: initial?.enabled ?? false,
2956 provider: initial?.provider ?? "openai",
2957 apiKey: initial?.apiKey ?? "",
2958 saving: false,
2959 error: ""
2960 };
2961 const el = document.createElement("div");
2962 const save = async () => {
2963 if (!url || !nonce || state.saving) {
2964 return;
2965 }
2966 state.saving = true;
2967 state.error = "";
2968 paint();
2969 try {
2970 const res = await trackedFetch(
2971 url,
2972 {
2973 method: "POST",
2974 headers: {
2975 "Content-Type": "application/json",
2976 "X-WP-Nonce": nonce
2977 },
2978 body: JSON.stringify({
2979 settings: {
2980 enabled: state.enabled,
2981 provider: state.provider,
2982 apiKey: state.apiKey
2983 }
2984 })
2985 },
2986 { source: "desktop-mode/settings/ai" }
2987 );
2988 if (!res.ok) {
2989 const err = await res.json().catch(() => ({}));
2990 state.error = err.message ?? `Error ${res.status}`;
2991 } else {
2992 const saved = await res.json().catch(() => null);
2993 if (saved && typeof saved === "object") {
2994 ctx.config.aiPlatformSettings = saved;
2995 }
2996 }
2997 } catch {
2998 state.error = __("Network error — check your connection.");
2999 } finally {
3000 state.saving = false;
3001 paint();
3002 }
3003 };
3004 const onToggle = (e) => {
3005 state.enabled = e.detail?.checked === true;
3006 save();
3007 };
3008 const onProvider = (e) => {
3009 const id = e.detail?.value ?? "";
3010 if (!getAiProviders().some((p) => p.id === id)) {
3011 return;
3012 }
3013 state.provider = id;
3014 save();
3015 };
3016 const onApiKey = (e) => {
3017 state.apiKey = e.detail?.value ?? "";
3018 };
3019 const onApiKeyCommit = () => {
3020 save();
3021 };
3022 const paint = () => render(
3023 html`
3024 <wpd-section
3025 heading=${__("Global settings")}
3026 description=${__("Platform-wide AI configuration. Applies to all users and to background jobs (cron, WP-CLI, anonymous comments). Individual users can override with their own key above.")}
3027 >
3028 <wpd-checkbox-label
3029 label=${__("Enable AI for all users")}
3030 ?checked=${state.enabled}
3031 @wpd-checkbox-change=${onToggle}
3032 ></wpd-checkbox-label>
3033
3034 <wpd-select
3035 label=${__("Provider")}
3036 value=${state.provider}
3037 ?disabled=${!state.enabled || state.saving}
3038 @wpd-pick=${onProvider}
3039 >
3040 ${getAiProviders().map(
3041 (p) => html`<wpd-option value=${p.id}>${p.label}</wpd-option>`
3042 )}
3043 </wpd-select>
3044
3045 <wpd-text-field
3046 label=${__("Platform API key")}
3047 type="password"
3048 reveal
3049 autocomplete="off"
3050 placeholder=${__("sk-…")}
3051 value=${state.apiKey}
3052 ?disabled=${!state.enabled || state.saving}
3053 @wpd-input-change=${onApiKey}
3054 @wpd-input-commit=${onApiKeyCommit}
3055 @wpd-submit=${onApiKeyCommit}
3056 ></wpd-text-field>
3057
3058 ${state.error ? html`<p class="desktop-mode-ai-settings__error">${state.error}</p>` : html``}
3059 ${state.saving ? html`<p class="desktop-mode-ai-settings__saving">${__("Saving…")}</p>` : html``}
3060 </wpd-section>
3061 `,
3062 el
3063 );
3064 paint();
3065 return el;
3066 }
3067 function hashTitleToHue(input) {
3068 if (!input) {
3069 return 214;
3070 }
3071 let hash = 5381;
3072 for (let i = 0; i < input.length; i++) {
3073 hash = Math.imul(hash, 33) + input.charCodeAt(i);
3074 }
3075 return (hash % 360 + 360) % 360;
3076 }
3077 function renderIcon(icon, opts) {
3078 const className = opts.className ?? "";
3079 const title = opts.title ?? "";
3080 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
3081 const el = document.createElement("span");
3082 el.className = `dashicons ${icon} ${className}`.trim();
3083 el.setAttribute("aria-hidden", "true");
3084 return el;
3085 }
3086 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
3087 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
3088 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
3089 const el = document.createElement("span");
3090 el.className = className;
3091 el.setAttribute("aria-hidden", "true");
3092 el.style.backgroundImage = `url("${icon}")`;
3093 el.style.backgroundRepeat = "no-repeat";
3094 el.style.backgroundPosition = "center";
3095 el.style.backgroundSize = "contain";
3096 el.style.display = "inline-block";
3097 return el;
3098 }
3099 }
3100 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
3101 const commaIdx = icon.indexOf(",");
3102 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
3103 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
3104 return makeImgIcon(icon, className);
3105 }
3106 }
3107 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
3108 return makeImgIcon(icon, className);
3109 }
3110 const span = document.createElement("span");
3111 span.className = `${className} desktop-mode-icon-letter`.trim();
3112 span.setAttribute("aria-hidden", "true");
3113 const letters = letterFromTitle(title);
3114 span.textContent = letters;
3115 const hue = hashTitleToHue(title);
3116 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
3117 span.style.color = "#fff";
3118 span.style.display = "inline-flex";
3119 span.style.alignItems = "center";
3120 span.style.justifyContent = "center";
3121 span.style.fontWeight = "600";
3122 span.style.borderRadius = "4px";
3123 return span;
3124 }
3125 function makeImgIcon(src, className) {
3126 const img = document.createElement("img");
3127 img.className = className;
3128 img.src = src;
3129 img.alt = "";
3130 img.setAttribute("aria-hidden", "true");
3131 img.draggable = false;
3132 return img;
3133 }
3134 function letterFromTitle(title) {
3135 const trimmed = (title ?? "").trim();
3136 if (trimmed === "") {
3137 return "?";
3138 }
3139 const words = trimmed.split(/\s+/);
3140 if (words.length >= 2) {
3141 return (words[0][0] + words[1][0]).toUpperCase();
3142 }
3143 const first = words[0];
3144 if (first.length >= 2) {
3145 return first.slice(0, 2).toUpperCase();
3146 }
3147 return first.toUpperCase();
3148 }
3149 function resolvePlacement(id, nativeRail, visibility) {
3150 const override = visibility[id];
3151 if (override) {
3152 return override;
3153 }
3154 return nativeRail;
3155 }
3156 function listPlaceableItems(dockItems, desktopIcons, visibility) {
3157 const out = [];
3158 const seen = /* @__PURE__ */ new Set();
3159 for (const item of dockItems) {
3160 if (seen.has(item.id)) {
3161 continue;
3162 }
3163 seen.add(item.id);
3164 out.push({
3165 id: item.id,
3166 title: item.title,
3167 icon: item.icon,
3168 nativeRail: "dock",
3169 placement: resolvePlacement(item.id, "dock", visibility)
3170 });
3171 }
3172 for (const icon of desktopIcons) {
3173 if (seen.has(icon.id)) {
3174 continue;
3175 }
3176 seen.add(icon.id);
3177 out.push({
3178 id: icon.id,
3179 title: icon.title,
3180 icon: icon.icon,
3181 nativeRail: "desktop",
3182 placement: resolvePlacement(icon.id, "desktop", visibility)
3183 });
3184 }
3185 out.sort(
3186 (a, b) => a.title.localeCompare(b.title, void 0, { sensitivity: "base" })
3187 );
3188 return out;
3189 }
3190 function readDockItems() {
3191 const api = window.wp?.desktop;
3192 if (api && typeof api.getMenuItems === "function") {
3193 return api.getMenuItems();
3194 }
3195 const cfg = window.desktopModeConfig;
3196 const raw = cfg?.dockItems ?? [];
3197 return raw.map((i) => ({
3198 id: i.id,
3199 title: i.title,
3200 icon: i.icon,
3201 url: i.url,
3202 badge: i.badge,
3203 submenu: i.submenu,
3204 multi: i.multi,
3205 isCore: i.isCore
3206 }));
3207 }
3208 function readDesktopIcons() {
3209 const cfg = window.desktopModeConfig;
3210 return cfg?.desktopIcons ?? [];
3211 }
3212 function getPlacementOptions() {
3213 return [
3214 { id: "desktop", label: __("On the desktop") },
3215 { id: "dock", label: __("On the dock") },
3216 { id: "both", label: __("On both") },
3217 { id: "hidden", label: __("Hidden") }
3218 ];
3219 }
3220 function buildAppsIconsSection(ctx) {
3221 const wrapper = document.createElement("div");
3222 const setPlacement = (id, placement) => {
3223 const next = { ...ctx.state.itemVisibility };
3224 next[id] = placement;
3225 ctx.state.itemVisibility = next;
3226 ctx.save();
3227 paint();
3228 };
3229 const onPlacementChange = (id) => (e) => {
3230 const detail = e.detail;
3231 const next = detail?.value;
3232 if (next === "both" || next === "dock" || next === "desktop" || next === "hidden") {
3233 setPlacement(id, next);
3234 }
3235 };
3236 const paint = () => {
3237 const dockItems = readDockItems();
3238 const desktopIcons = readDesktopIcons();
3239 const rows = listPlaceableItems(
3240 dockItems,
3241 desktopIcons,
3242 ctx.state.itemVisibility
3243 );
3244 render(
3245 html`
3246 <wpd-section
3247 heading=${__("Apps & Icons")}
3248 description=${__(
3249 "Choose where each app shortcut shows up — on the dock, on the desktop wallpaper, both, or hidden entirely. Changes apply instantly to the running shell."
3250 )}
3251 >
3252 ${rows.length === 0 ? html`<wpd-empty-state
3253 heading=${__("No apps registered yet")}
3254 description=${__(
3255 "Plugins and the admin menu will appear here once they’re registered."
3256 )}
3257 ></wpd-empty-state>` : html`<div class="desktop-mode-apps-icons__list">
3258 ${rows.map(
3259 (row) => html`<div
3260 class="desktop-mode-apps-icons__row"
3261 data-item-id=${row.id}
3262 >
3263 <div class="desktop-mode-apps-icons__identity">
3264 ${renderIcon(row.icon, {
3265 title: row.title,
3266 className: "desktop-mode-apps-icons__icon"
3267 })}
3268 <div class="desktop-mode-apps-icons__title">
3269 ${row.title}
3270 </div>
3271 </div>
3272 <wpd-select
3273 label=${__("Show in")}
3274 value=${row.placement}
3275 @wpd-pick=${onPlacementChange(row.id)}
3276 >
3277 ${getPlacementOptions().map(
3278 (o) => html`<wpd-option
3279 value=${o.id}
3280 >${o.label}</wpd-option
3281 >`
3282 )}
3283 </wpd-select>
3284 </div>`
3285 )}
3286 </div>`}
3287 </wpd-section>
3288 `,
3289 wrapper
3290 );
3291 };
3292 paint();
3293 return wrapper;
3294 }
3295 function buildDesktopLayoutSection(ctx) {
3296 const onPick = (e) => {
3297 const id = e.detail?.value ?? "";
3298 if (!DESKTOP_LAYOUTS.some((l) => l.id === id)) {
3299 return;
3300 }
3301 ctx.state.desktopLayout = id;
3302 ctx.save();
3303 ctx.apply();
3304 paint();
3305 };
3306 const wrapper = document.createElement("div");
3307 const paint = () => render(
3308 html`
3309 <wpd-section
3310 heading=${__("Desktop layout")}
3311 description=${translateDesktopLayoutDescription(
3312 ctx.state.desktopLayout
3313 )}
3314 >
3315 <wpd-segmented
3316 value=${ctx.state.desktopLayout}
3317 label=${__("Desktop layout")}
3318 @wpd-pick=${onPick}
3319 >
3320 ${DESKTOP_LAYOUTS.map(
3321 (l) => html`<wpd-segment value=${l.id}
3322 >${translateDesktopLayoutLabel(
3323 l.id,
3324 l.label
3325 )}</wpd-segment
3326 >`
3327 )}
3328 </wpd-segmented>
3329 </wpd-section>
3330 `,
3331 wrapper
3332 );
3333 paint();
3334 return wrapper;
3335 }
3336 function buildDockSizeSection(ctx) {
3337 const onPick = (e) => {
3338 const id = e.detail?.value ?? "";
3339 if (!DOCK_SIZES.some((d) => d.id === id)) {
3340 return;
3341 }
3342 ctx.state.dockSize = id;
3343 ctx.save();
3344 ctx.apply();
3345 paint();
3346 };
3347 const wrapper = document.createElement("div");
3348 const paint = () => render(
3349 html`
3350 <wpd-section
3351 heading=${__("Dock size")}
3352 description=${__("Width of the dock and size of its icons.")}
3353 >
3354 <wpd-segmented
3355 value=${ctx.state.dockSize}
3356 label=${__("Dock size")}
3357 @wpd-pick=${onPick}
3358 >
3359 ${DOCK_SIZES.map(
3360 (s) => html`<wpd-segment value=${s.id}
3361 >${translateDockSizeLabel(s.id, s.label)}</wpd-segment
3362 >`
3363 )}
3364 </wpd-segmented>
3365 </wpd-section>
3366 `,
3367 wrapper
3368 );
3369 paint();
3370 return wrapper;
3371 }
3372 const store$1 = createSharedStore(
3373 "desktop-mode/dock-rail-registry",
3374 () => ({
3375 registry: /* @__PURE__ */ new Map(),
3376 listeners: /* @__PURE__ */ new Set(),
3377 activeId: "default"
3378 })
3379 );
3380 const registry = store$1.state.registry;
3381 const listeners$1 = store$1.state.listeners;
3382 function list() {
3383 return Array.from(registry.values());
3384 }
3385 function subscribe$1(cb) {
3386 listeners$1.add(cb);
3387 return () => {
3388 listeners$1.delete(cb);
3389 };
3390 }
3391 function getWpHooks() {
3392 const hooks = window.wp?.hooks;
3393 if (!hooks) {
3394 throw new Error(
3395 "[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."
3396 );
3397 }
3398 return hooks;
3399 }
3400 function addAction(hookName2, namespace, callback, priority) {
3401 getWpHooks().addAction(
3402 hookName2,
3403 namespace,
3404 callback,
3405 priority
3406 );
3407 }
3408 function removeAction(hookName2, namespace) {
3409 return getWpHooks().removeAction(hookName2, namespace);
3410 }
3411 function applyFilters(hookName2, value, ...args) {
3412 return getWpHooks().applyFilters(hookName2, value, ...args);
3413 }
3414 function doAction(hookName2, ...args) {
3415 getWpHooks().doAction(hookName2, ...args);
3416 }
3417 const HOOKS = {
3418 /** Filter, receives the wallpaper registry array. */
3419 WALLPAPERS: "desktop-mode.wallpapers"
3420 };
3421 const HOOK_PREFIX = "desktop-mode.activity.";
3422 function hookName(channel) {
3423 return `${HOOK_PREFIX}${String(channel)}`;
3424 }
3425 let subscribeSeq = 0;
3426 const activity = {
3427 publish(channel, payload) {
3428 doAction(hookName(channel), payload);
3429 },
3430 subscribe(channel, cb) {
3431 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
3432 const hook = hookName(channel);
3433 addAction(
3434 hook,
3435 ns,
3436 (payload) => cb(payload)
3437 );
3438 let removed = false;
3439 return () => {
3440 if (removed) {
3441 return;
3442 }
3443 removed = true;
3444 removeAction(hook, ns);
3445 };
3446 },
3447 filter(channel, value, ...args) {
3448 return applyFilters(hookName(channel), value, ...args);
3449 }
3450 };
3451 const CANARY_TAG = "wpd-confirm-dialog";
3452 let inflight = null;
3453 function isLoaded() {
3454 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
3455 }
3456 function injectScript(scriptUrl) {
3457 return new Promise((resolve, reject) => {
3458 const existing = document.querySelector(
3459 'script[data-desktop-mode-shell-overlays="1"]'
3460 );
3461 const finish = () => {
3462 if (isLoaded()) {
3463 resolve();
3464 return;
3465 }
3466 reject(
3467 new Error(
3468 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
3469 )
3470 );
3471 };
3472 if (existing) {
3473 if (isLoaded()) {
3474 finish();
3475 } else {
3476 existing.addEventListener("load", finish);
3477 existing.addEventListener(
3478 "error",
3479 () => reject(new Error("failed to load shell-overlays bundle"))
3480 );
3481 }
3482 return;
3483 }
3484 const s = document.createElement("script");
3485 s.src = scriptUrl;
3486 s.async = true;
3487 s.dataset.desktopModeShellOverlays = "1";
3488 s.addEventListener("load", finish);
3489 s.addEventListener(
3490 "error",
3491 () => reject(new Error("failed to load shell-overlays bundle"))
3492 );
3493 document.head.appendChild(s);
3494 });
3495 }
3496 function ensureShellOverlaysLoaded(scriptUrl) {
3497 if (isLoaded()) {
3498 return Promise.resolve();
3499 }
3500 if (!scriptUrl) {
3501 return Promise.resolve();
3502 }
3503 if (!inflight) {
3504 inflight = injectScript(scriptUrl);
3505 }
3506 return inflight;
3507 }
3508 function shellOverlaysBundleUrl() {
3509 const cfg = window.desktopModeConfig;
3510 return cfg?.shellOverlaysBundleUrl ?? "";
3511 }
3512 function openWithShellOverlays(isStillCurrent, fn) {
3513 const url = shellOverlaysBundleUrl();
3514 if (isLoaded() || !url) {
3515 fn();
3516 return;
3517 }
3518 void ensureShellOverlaysLoaded(url).then(() => {
3519 if (!isStillCurrent()) {
3520 return;
3521 }
3522 fn();
3523 }).catch((err) => {
3524 if (typeof console !== "undefined") {
3525 console.warn(
3526 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
3527 err
3528 );
3529 }
3530 });
3531 }
3532 const dialogStyles = css`:host{display:none;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba( 0,0,0,0.45 );backdrop-filter:blur( 2px );z-index:10000}:host( [ open ] ){display:flex}.dialog{width:min( 420px,92vw );background:var( --wpd-confirm-dialog-bg,var( --desktop-mode-bg,#1d2327 ) );color:var( --wpd-confirm-dialog-fg,var( --desktop-mode-fg,#fff ) );border:1px solid rgba( 255,255,255,0.08 );border-radius:10px;box-shadow:0 20px 50px rgba( 0,0,0,0.6 );padding:20px 22px 18px;display:flex;flex-direction:column;gap:10px;position:relative}.close{position:absolute;top:8px;right:10px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;border-radius:6px;color:var( --wpd-confirm-dialog-fg-muted,rgba( 255,255,255,0.7 ) );cursor:pointer;font-size:22px;line-height:1;padding:0}.close:hover{background:rgba( 255,255,255,0.08 );color:inherit}.title{margin:0 0 4px;font-size:16px;font-weight:600}.message{margin:0;color:var( --wpd-confirm-dialog-fg-muted,rgba( 255,255,255,0.7 ) );line-height:1.45;white-space:pre-line}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:6px}.btn{border:0;border-radius:6px;padding:8px 14px;font-size:13px;cursor:pointer;font-weight:500}.btn--secondary{background:rgba( 255,255,255,0.08 );color:inherit}.btn--secondary:hover{background:rgba( 255,255,255,0.14 )}.btn--primary{background:var( --wp-admin-theme-color,#2271b1 );color:#fff}.btn--primary:hover{filter:brightness( 1.08 )}.btn--danger{background:#d63638;color:#fff}.btn--danger:hover{filter:brightness( 1.08 )}`;
3533 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
3534 constructor() {
3535 super(...arguments);
3536 this._onKey = (e) => {
3537 if (e.key === "Escape") {
3538 e.preventDefault();
3539 this._cancel();
3540 }
3541 if (e.key === "Enter" && !e.isComposing) {
3542 e.preventDefault();
3543 this._confirm();
3544 }
3545 };
3546 this._onBackdrop = (e) => {
3547 const path = e.composedPath();
3548 const original = path.length > 0 ? path[0] : e.target;
3549 if (original === this) {
3550 this._cancel();
3551 }
3552 };
3553 this._confirm = () => {
3554 this.emit("wpd-confirm", { confirmed: true });
3555 this.removeAttribute("open");
3556 };
3557 this._cancel = () => {
3558 this.emit("wpd-cancel", { confirmed: false });
3559 this.removeAttribute("open");
3560 };
3561 }
3562 connectedCallback() {
3563 super.connectedCallback();
3564 this.setAttribute("role", "dialog");
3565 this.setAttribute("aria-modal", "true");
3566 this.addEventListener("keydown", this._onKey);
3567 this.addEventListener("click", this._onBackdrop);
3568 }
3569 disconnectedCallback() {
3570 this.removeEventListener("keydown", this._onKey);
3571 this.removeEventListener("click", this._onBackdrop);
3572 }
3573 render() {
3574 const title = this.title ?? "";
3575 const message = this.message ?? "";
3576 const confirmLabel = this["confirm-label"] || "Confirm";
3577 const cancelLabel = this["cancel-label"] || "Cancel";
3578 const isDanger = this.hasAttribute("danger");
3579 const hideCancel = this.hasAttribute("hide-cancel");
3580 const isDismissable = this.hasAttribute("dismissable");
3581 return html`
3582 <div class="dialog" tabindex="-1">
3583 ${isDismissable ? html`<button
3584 type="button"
3585 class="close"
3586 aria-label="Close"
3587 @click=${() => this._cancel()}
3588 >&times;</button>` : html``}
3589 ${title ? html`<h2 class="title">${title}</h2>` : html``}
3590 ${message ? html`<p class="message">${message}</p>` : html``}
3591 <div class="actions">
3592 ${hideCancel ? html`` : html`<button
3593 type="button"
3594 class="btn btn--secondary"
3595 @click=${() => this._cancel()}
3596 >
3597 ${cancelLabel}
3598 </button>`}
3599 <button
3600 type="button"
3601 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
3602 @click=${() => this._confirm()}
3603 >
3604 ${confirmLabel}
3605 </button>
3606 </div>
3607 </div>
3608 `;
3609 }
3610 };
3611 _WpdConfirmDialog.props = [
3612 "open",
3613 "title",
3614 "message",
3615 "confirm-label",
3616 "cancel-label",
3617 "danger",
3618 "hide-cancel",
3619 "dismissable"
3620 ];
3621 _WpdConfirmDialog.styles = [dialogStyles];
3622 _WpdConfirmDialog.help = {
3623 title: "Confirm dialog",
3624 summary: "Modal Yes/No replacement for window.confirm(). Two consumption paths: declarative element with `open` + `wpd-confirm` event, or the imperative Promise-returning `wpdConfirm()` helper.",
3625 status: "experimental",
3626 since: "0.9.0",
3627 props: [
3628 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
3629 { name: "title", type: "string", description: "Heading shown at the top." },
3630 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
3631 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
3632 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
3633 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
3634 { name: "hide-cancel", type: "boolean attribute", description: "Hides the cancel button entirely. Useful when there is no alternative action — pair with `dismissable` so the user still has an explicit way to close." },
3635 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
3636 ],
3637 events: [
3638 {
3639 name: "wpd-confirm",
3640 description: "Fires on confirm. Detail: `{ confirmed: true }`."
3641 },
3642 {
3643 name: "wpd-cancel",
3644 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
3645 }
3646 ]
3647 };
3648 let WpdConfirmDialog = _WpdConfirmDialog;
3649 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
3650 function wpdConfirm(options) {
3651 return new Promise((resolve) => {
3652 const dialog = document.createElement("wpd-confirm-dialog");
3653 dialog.setAttribute("open", "");
3654 if (options.title) {
3655 dialog.setAttribute("title", options.title);
3656 }
3657 dialog.setAttribute("message", options.message);
3658 if (options.confirmLabel) {
3659 dialog.setAttribute("confirm-label", options.confirmLabel);
3660 }
3661 if (options.cancelLabel) {
3662 dialog.setAttribute("cancel-label", options.cancelLabel);
3663 }
3664 {
3665 dialog.setAttribute("danger", "");
3666 }
3667 if (options.hideCancel) {
3668 dialog.setAttribute("hide-cancel", "");
3669 }
3670 if (options.dismissable) {
3671 dialog.setAttribute("dismissable", "");
3672 }
3673 const cleanup = (ok) => {
3674 dialog.remove();
3675 resolve(ok);
3676 };
3677 dialog.addEventListener("wpd-confirm", () => cleanup(true));
3678 dialog.addEventListener("wpd-cancel", () => cleanup(false));
3679 document.body.appendChild(dialog);
3680 const inner = dialog.shadowRoot?.querySelector(".dialog");
3681 (inner ?? dialog).focus?.();
3682 });
3683 }
3684 const DEFAULT_DURATION_MS = 4e3;
3685 const FADE_OUT_MS = 200;
3686 function showToast(options) {
3687 const intent = activity.filter(
3688 "desktop-mode/toast-requested",
3689 { ...options }
3690 );
3691 if (!intent || intent.cancel === true) {
3692 return () => void 0;
3693 }
3694 let dismissRequested = false;
3695 let realDismiss = null;
3696 openWithShellOverlays(
3697 () => !dismissRequested,
3698 () => {
3699 realDismiss = renderToast(intent);
3700 }
3701 );
3702 return () => {
3703 dismissRequested = true;
3704 if (realDismiss) {
3705 realDismiss();
3706 }
3707 };
3708 }
3709 function renderToast(intent) {
3710 const container = ensureContainer();
3711 const toast = document.createElement("wpd-toast");
3712 toast.textContent = intent.message;
3713 if (intent.action) {
3714 toast.setAttribute("action", intent.action.label);
3715 toast.addEventListener("wpd-toast-action", () => {
3716 intent.action?.onClick();
3717 dismiss();
3718 });
3719 }
3720 container.appendChild(toast);
3721 let dismissed = false;
3722 let dismissTimer = null;
3723 const dismiss = () => {
3724 if (dismissed) {
3725 return;
3726 }
3727 dismissed = true;
3728 if (dismissTimer !== null) {
3729 window.clearTimeout(dismissTimer);
3730 dismissTimer = null;
3731 }
3732 toast.setAttribute("state", "out");
3733 window.setTimeout(() => {
3734 toast.remove();
3735 }, FADE_OUT_MS);
3736 };
3737 requestAnimationFrame(() => {
3738 toast.setAttribute("state", "in");
3739 });
3740 dismissTimer = window.setTimeout(
3741 dismiss,
3742 intent.duration ?? DEFAULT_DURATION_MS
3743 );
3744 activity.publish("desktop-mode/toast-shown", { ...intent });
3745 return dismiss;
3746 }
3747 function ensureContainer() {
3748 const existing = document.querySelector(
3749 "wpd-toast-container"
3750 );
3751 if (existing) {
3752 return existing;
3753 }
3754 const el = document.createElement("wpd-toast-container");
3755 document.body.appendChild(el);
3756 return el;
3757 }
3758 createSharedStore(
3759 "desktop-mode/native-url-remap",
3760 () => ({ remaps: [], deps: null })
3761 );
3762 function buildDockRailRendererSection(ctx) {
3763 const wrapper = document.createElement("div");
3764 const onPick = (e) => {
3765 const id = e.detail?.value ?? "";
3766 if (id === "") {
3767 return;
3768 }
3769 ctx.state.dockRailRenderer = id;
3770 ctx.save();
3771 ctx.apply();
3772 paint();
3773 };
3774 let renderers = list();
3775 const paint = () => {
3776 if (renderers.length <= 1) {
3777 render(html``, wrapper);
3778 return;
3779 }
3780 render(
3781 html`
3782 <wpd-section
3783 heading=${__("Dock style")}
3784 description=${__(
3785 "How the rail itself paints — the shipped icon strip, or anything a plugin replaces it with. Switching is instant; the dock rebuilds with the new renderer."
3786 )}
3787 >
3788 <wpd-segmented
3789 value=${ctx.state.dockRailRenderer}
3790 label=${__("Dock style")}
3791 @wpd-pick=${onPick}
3792 >
3793 ${renderers.map(
3794 (r) => html`<wpd-segment value=${r.id}
3795 >${r.label}</wpd-segment
3796 >`
3797 )}
3798 </wpd-segmented>
3799 </wpd-section>
3800 `,
3801 wrapper
3802 );
3803 };
3804 const unsubscribe = subscribe$1(() => {
3805 renderers = list();
3806 paint();
3807 });
3808 const observer = new MutationObserver(() => {
3809 if (!wrapper.isConnected) {
3810 unsubscribe();
3811 observer.disconnect();
3812 }
3813 });
3814 queueMicrotask(() => {
3815 if (wrapper.parentNode) {
3816 observer.observe(wrapper.parentNode, {
3817 childList: true,
3818 subtree: false
3819 });
3820 }
3821 });
3822 paint();
3823 return wrapper;
3824 }
3825 function buildExtendedSection(ctx) {
3826 const { extendedOptions, extendedOptionsUrl, restNonce } = ctx.config;
3827 const state = {
3828 media_library_enhanced: extendedOptions?.media_library_enhanced === true,
3829 saving: false,
3830 error: ""
3831 };
3832 const el = document.createElement("div");
3833 const save = async () => {
3834 if (!extendedOptionsUrl || !restNonce || state.saving) {
3835 return;
3836 }
3837 state.saving = true;
3838 state.error = "";
3839 paint();
3840 try {
3841 const res = await trackedFetch(
3842 extendedOptionsUrl,
3843 {
3844 method: "POST",
3845 headers: {
3846 "Content-Type": "application/json",
3847 "X-WP-Nonce": restNonce
3848 },
3849 body: JSON.stringify({
3850 options: {
3851 media_library_enhanced: state.media_library_enhanced
3852 }
3853 })
3854 },
3855 { source: "desktop-mode/settings/extended" }
3856 );
3857 if (!res.ok) {
3858 const err = await res.json().catch(() => ({}));
3859 state.error = err.message ?? `Error ${res.status}`;
3860 } else {
3861 const saved = await res.json().catch(() => null);
3862 if (saved && typeof saved === "object") {
3863 ctx.config.extendedOptions = saved;
3864 }
3865 }
3866 } catch {
3867 state.error = __("Network error — check your connection.");
3868 } finally {
3869 state.saving = false;
3870 paint();
3871 }
3872 };
3873 const onMediaToggle = (e) => {
3874 state.media_library_enhanced = e.detail?.checked === true;
3875 save();
3876 };
3877 const paint = () => render(
3878 html`
3879 <wpd-section
3880 heading=${__("Extended options")}
3881 description=${__(
3882 "Site-wide enhancements that apply to every user. Toggling requires the affected page to be reloaded for the change to take effect."
3883 )}
3884 >
3885 <wpd-checkbox-label
3886 label=${__("Enable drag-and-drop in the Media Library")}
3887 ?checked=${state.media_library_enhanced}
3888 @wpd-checkbox-change=${onMediaToggle}
3889 ></wpd-checkbox-label>
3890
3891 <p class="desktop-mode-ext__hint">
3892 ${__(
3893 "Makes every item in the WordPress Media Library draggable. Drop a media item into text fields, rich-text editors, Gutenberg blocks, or any target that accepts images or files. No replacement of the library — just a drag-and-drop layer on top of the one you already know."
3894 )}
3895 </p>
3896
3897 ${state.error ? html`<p class="desktop-mode-ext__error">${state.error}</p>` : html``}
3898 ${state.saving ? html`<p class="desktop-mode-ext__saving">${__("Saving…")}</p>` : html``}
3899 </wpd-section>
3900 `,
3901 el
3902 );
3903 paint();
3904 return el;
3905 }
3906 function buildFeaturesSection(ctx) {
3907 const wrapper = document.createElement("div");
3908 const onNativePostsToggle = (e) => {
3909 const checked = e.detail?.checked === true;
3910 ctx.state.nativePostsEnabled = checked;
3911 ctx.save();
3912 paint();
3913 };
3914 const onHeartbeatRateChange = (e) => {
3915 const raw = e.detail?.value;
3916 const next = Number(raw);
3917 if (![15, 30, 45, 60].includes(next)) {
3918 return;
3919 }
3920 ctx.state.heartbeatRate = next;
3921 ctx.save();
3922 try {
3923 const wp = window.wp;
3924 const speed = next >= 60 ? "slow" : "standard";
3925 wp?.heartbeat?.interval?.(speed);
3926 } catch (_e) {
3927 }
3928 paint();
3929 };
3930 const onNativePagesToggle = (e) => {
3931 const checked = e.detail?.checked === true;
3932 ctx.state.nativePagesEnabled = checked;
3933 ctx.save();
3934 paint();
3935 };
3936 const onNativeUsersToggle = (e) => {
3937 const checked = e.detail?.checked === true;
3938 ctx.state.nativeUsersEnabled = checked;
3939 ctx.save();
3940 paint();
3941 };
3942 const onNativePluginsToggle = (e) => {
3943 const checked = e.detail?.checked === true;
3944 ctx.state.nativePluginsEnabled = checked;
3945 ctx.save();
3946 paint();
3947 };
3948 const onNativeCommentsToggle = (e) => {
3949 const checked = e.detail?.checked === true;
3950 ctx.state.nativeCommentsEnabled = checked;
3951 ctx.save();
3952 paint();
3953 };
3954 const onShowDesktopOnClickToggle = (e) => {
3955 const checked = e.detail?.checked === true;
3956 ctx.state.showDesktopOnWallpaperClick = checked;
3957 ctx.save();
3958 paint();
3959 };
3960 const onShowPostStatusRibbonsToggle = (e) => {
3961 const checked = e.detail?.checked === true;
3962 ctx.state.showPostStatusRibbons = checked;
3963 ctx.save();
3964 paint();
3965 };
3966 const onFolderSharingToggle = (e) => {
3967 const checked = e.detail?.checked === true;
3968 ctx.state.foldersSharingEnabled = checked;
3969 ctx.save();
3970 paint();
3971 };
3972 let purging = false;
3973 const onPurgeShareTables = async () => {
3974 if (purging) {
3975 return;
3976 }
3977 const ok = await wpdConfirm({
3978 title: __("Delete folder sharing data?"),
3979 message: __(
3980 "This drops every shares table on the site (current + legacy). All invites, accept/deny decisions, and share rows are permanently removed. Recipients lose their access until someone shares with them again. The empty tables are recreated on the next admin load so the feature keeps working — but every existing share is gone."
3981 ),
3982 confirmLabel: __("Delete data")
3983 });
3984 if (!ok) {
3985 return;
3986 }
3987 const base = shellCfg?.filesUrl;
3988 const nonce = shellCfg?.restNonce;
3989 if (!base || !nonce) {
3990 showToast({ message: __("Files REST endpoint is not available.") });
3991 return;
3992 }
3993 purging = true;
3994 paint();
3995 try {
3996 const url = base.replace(/\/+$/, "") + "/folder-sharing-tables/purge";
3997 const res = await trackedFetch(
3998 url,
3999 {
4000 method: "POST",
4001 headers: { "X-WP-Nonce": nonce },
4002 credentials: "same-origin"
4003 },
4004 { source: "os-settings/folder-sharing-purge" }
4005 );
4006 if (!res.ok) {
4007 const body = await res.text();
4008 throw new Error(`${res.status}: ${body.slice(0, 200)}`);
4009 }
4010 const data = await res.json();
4011 showToast({
4012 message: __("Folder sharing data deleted.") + " (" + data.dropped.length + " tables)"
4013 });
4014 } catch (err) {
4015 const detail = err instanceof Error ? err.message : String(err);
4016 showToast({
4017 message: __("Could not delete sharing data.") + " " + detail
4018 });
4019 } finally {
4020 purging = false;
4021 paint();
4022 }
4023 };
4024 const shellCfg = window.desktopModeConfig;
4025 const aiState = {
4026 enabled: shellCfg?.commentsAi?.enabled ?? false,
4027 providerConfigured: shellCfg?.commentsAi?.providerConfigured ?? false,
4028 saving: false
4029 };
4030 const onCommentsAiToggle = async (e) => {
4031 const checked = e.detail?.checked === true;
4032 if (!shellCfg?.commentsAiUrl || aiState.saving) {
4033 return;
4034 }
4035 aiState.saving = true;
4036 aiState.enabled = checked;
4037 paint();
4038 try {
4039 const response = await trackedFetch(
4040 shellCfg.commentsAiUrl,
4041 {
4042 method: "POST",
4043 credentials: "same-origin",
4044 headers: {
4045 "Content-Type": "application/json",
4046 "X-WP-Nonce": shellCfg.restNonce ?? ""
4047 },
4048 body: JSON.stringify({ enabled: checked })
4049 },
4050 { source: "os-settings/comments-ai" }
4051 );
4052 if (response.ok) {
4053 const json = await response.json();
4054 aiState.enabled = json.enabled;
4055 aiState.providerConfigured = json.providerConfigured;
4056 if (shellCfg.commentsAi) {
4057 shellCfg.commentsAi.enabled = json.enabled;
4058 shellCfg.commentsAi.providerConfigured = json.providerConfigured;
4059 }
4060 } else {
4061 aiState.enabled = !checked;
4062 }
4063 } catch {
4064 aiState.enabled = !checked;
4065 }
4066 aiState.saving = false;
4067 paint();
4068 };
4069 let resetting = false;
4070 const onResetIntros = async () => {
4071 if (resetting) {
4072 return;
4073 }
4074 const cfg = window.desktopModeConfig;
4075 if (!cfg?.seenIntrosUrl) {
4076 return;
4077 }
4078 resetting = true;
4079 paint();
4080 try {
4081 await trackedFetch(
4082 cfg.seenIntrosUrl,
4083 {
4084 method: "DELETE",
4085 credentials: "same-origin",
4086 headers: {
4087 "X-WP-Nonce": cfg.restNonce ?? ""
4088 }
4089 },
4090 { source: "os-settings/reset-intros" }
4091 );
4092 const store2 = window.desktopModeWindowConfig;
4093 if (store2) {
4094 Object.values(store2).forEach((entry) => {
4095 if (entry && typeof entry === "object") {
4096 entry.introSeen = false;
4097 }
4098 });
4099 }
4100 document.dispatchEvent(
4101 new CustomEvent("desktop-mode-intros-reset")
4102 );
4103 } catch {
4104 }
4105 resetting = false;
4106 paint();
4107 };
4108 const paint = () => render(
4109 html`
4110 <wpd-section
4111 heading=${__("Features")}
4112 description=${__(
4113 "Tune individual Desktop Mode behaviors. Each toggle affects only your account and takes effect immediately — no reload required. Watch the dot in the OS Settings title bar to see when a change has been saved."
4114 )}
4115 >
4116 <div class="desktop-mode-features__item">
4117 <wpd-checkbox-label
4118 label=${__("Use the native Posts window")}
4119 ?checked=${ctx.state.nativePostsEnabled}
4120 @wpd-checkbox-change=${onNativePostsToggle}
4121 ></wpd-checkbox-label>
4122 <p class="desktop-mode-features__hint">
4123 ${__(
4124 "Replaces the classic Posts list iframe with a native, table-driven window: sticky header, server-paginated rows, multi-select bulk actions, and a sub-row preview. On by default. Toggle off to return to the classic experience."
4125 )}
4126 </p>
4127 </div>
4128 <div class="desktop-mode-features__item">
4129 <wpd-checkbox-label
4130 label=${__("Use the native Pages window")}
4131 ?checked=${ctx.state.nativePagesEnabled}
4132 @wpd-checkbox-change=${onNativePagesToggle}
4133 ></wpd-checkbox-label>
4134 <p class="desktop-mode-features__hint">
4135 ${__(
4136 "Same table-driven experience as the Posts window, tailored for Pages: a Parent column, hierarchical sort, and the same lock indicator when another user is editing a page. On by default. Toggle off to return to the classic experience."
4137 )}
4138 </p>
4139 </div>
4140 <div class="desktop-mode-features__item">
4141 <wpd-checkbox-label
4142 label=${__("Use the native Users window")}
4143 ?checked=${ctx.state.nativeUsersEnabled}
4144 @wpd-checkbox-change=${onNativeUsersToggle}
4145 ></wpd-checkbox-label>
4146 <p class="desktop-mode-features__hint">
4147 ${__(
4148 "A native Users list with bulk role change, last-login tracking, live online indicators, click-to-copy email, and one-click password resets. Capability-gated — readers see a read-only view, role assignment respects WordPress role permissions. On by default."
4149 )}
4150 </p>
4151 </div>
4152 <div class="desktop-mode-features__item">
4153 <wpd-checkbox-label
4154 label=${__("Use the native Plugins window")}
4155 ?checked=${ctx.state.nativePluginsEnabled}
4156 @wpd-checkbox-change=${onNativePluginsToggle}
4157 ></wpd-checkbox-label>
4158 <p class="desktop-mode-features__hint">
4159 ${__(
4160 "A native two-tab Plugins window: an Installed list with bulk activate / deactivate / delete, and a Browse gallery powered by the WordPress.org repository — rich detail flyout with screenshots, ratings histogram, and recent reviews. Drag a .zip onto the window to install, or drag a card from Browse to the dock to pin it. On by default."
4161 )}
4162 </p>
4163 </div>
4164 <div class="desktop-mode-features__item">
4165 <wpd-checkbox-label
4166 label=${__("Use the native Comments window")}
4167 ?checked=${ctx.state.nativeCommentsEnabled}
4168 @wpd-checkbox-change=${onNativeCommentsToggle}
4169 ></wpd-checkbox-label>
4170 <p class="desktop-mode-features__hint">
4171 ${__(
4172 "A redesigned moderation queue with Pending / All / Spam / Trash / Mine tabs, bulk approve/spam/trash plus an 8-second undo, inline reply right in the row, an author insights drawer, a per-row spam confidence score (Akismet + heuristics), and full keyboard moderation (j/k navigate, a approve, s spam, d trash, r reply, e edit, u undo). On by default."
4173 )}
4174 </p>
4175 </div>
4176 ${shellCfg?.commentsAi ? html`
4177 <div class="desktop-mode-features__item">
4178 <wpd-checkbox-label
4179 label=${__("Score new comments with AI")}
4180 ?checked=${aiState.enabled}
4181 ?disabled=${aiState.saving || !aiState.providerConfigured}
4182 @wpd-checkbox-change=${onCommentsAiToggle}
4183 ></wpd-checkbox-label>
4184 <p class="desktop-mode-features__hint">
4185 ${aiState.providerConfigured ? __(
4186 "When a new comment lands, your configured AI provider scores it for spam and hostility. The verdict appears in the per-row chip and is folded into the spam confidence score. Token usage applies — admin-only site setting."
4187 ) : __(
4188 "Configure an AI provider in OS Settings → AI first. Once a provider is set up, this toggle becomes available and every new comment is scored on arrival."
4189 )}
4190 </p>
4191 </div>
4192 ` : ""}
4193 <div class="desktop-mode-features__item">
4194 <wpd-checkbox-label
4195 label=${__(
4196 "Show desktop when clicking the wallpaper"
4197 )}
4198 ?checked=${ctx.state.showDesktopOnWallpaperClick}
4199 @wpd-checkbox-change=${onShowDesktopOnClickToggle}
4200 ></wpd-checkbox-label>
4201 <p class="desktop-mode-features__hint">
4202 ${__(
4203 'macOS-style gesture: a left click on the empty desktop minimizes every window, and a second click restores them. When on, the matching "Show desktop" entry is removed from the wallpaper context menu — the click gesture replaces it. Off by default.'
4204 )}
4205 </p>
4206 </div>
4207 <div class="desktop-mode-features__item">
4208 <wpd-checkbox-label
4209 label=${__(
4210 "Show post/page status ribbon"
4211 )}
4212 ?checked=${ctx.state.showPostStatusRibbons}
4213 @wpd-checkbox-change=${onShowPostStatusRibbonsToggle}
4214 ></wpd-checkbox-label>
4215 <p class="desktop-mode-features__hint">
4216 ${__(
4217 "Paints a diagonal corner ribbon — Draft, Pending, Private, or Scheduled — on My WordPress tiles whose post status isn’t published. Off hides every ribbon; tiles still respect their dimmed-icon treatment so unpublished items remain visible at a glance. On by default."
4218 )}
4219 </p>
4220 </div>
4221 <div class="desktop-mode-features__item">
4222 <wpd-checkbox-label
4223 label=${__("Folder sharing")}
4224 ?checked=${ctx.state.foldersSharingEnabled}
4225 @wpd-checkbox-change=${onFolderSharingToggle}
4226 ></wpd-checkbox-label>
4227 <p class="desktop-mode-features__hint">
4228 ${__(
4229 'Lets you share desktop folders with other users or roles, with read or read+write access. When off, every share-related affordance (Share button, invites, "Leave shared folder") disappears from your shell and the heartbeat stops delivering share payloads to your session. Other users are unaffected. On by default.'
4230 )}
4231 </p>
4232 ${shellCfg?.currentUserIsAdmin ? html`
4233 <div class="desktop-mode-features__danger-row">
4234 <wpd-button
4235 variant="danger"
4236 ?disabled=${purging}
4237 @click=${onPurgeShareTables}
4238 >
4239 ${purging ? __("Deleting…") : __("Delete folder sharing data")}
4240 </wpd-button>
4241 <p class="desktop-mode-features__hint">
4242 ${__(
4243 "Site-wide destructive action (admin only). Drops every shares table — invites, decisions, share rows. Empty tables are recreated immediately so the feature still works for anyone who wants to start fresh. Use this on sites that never needed sharing to clear the data outright."
4244 )}
4245 </p>
4246 </div>
4247 ` : ""}
4248 </div>
4249 <div class="desktop-mode-features__item">
4250 <label class="desktop-mode-features__select-label">
4251 <span class="desktop-mode-features__select-title">${__(
4252 "WordPress Heartbeat rate"
4253 )}</span>
4254 <wpd-select
4255 value=${String(ctx.state.heartbeatRate)}
4256 @wpd-pick=${onHeartbeatRateChange}
4257 >
4258 <wpd-option value="15">${__("Fast — 15s (not recommended)")}</wpd-option>
4259 <wpd-option value="30">${__("Medium — 30s")}</wpd-option>
4260 <wpd-option value="45">${__("Slow — 45s")}</wpd-option>
4261 <wpd-option value="60">${__("Very slow — 60s (default)")}</wpd-option>
4262 </wpd-select>
4263 </label>
4264 <p class="desktop-mode-features__hint">
4265 ${__(
4266 "How often the WordPress Heartbeat API runs. Faster = quicker live updates (autosaves, lock checks, the heartbeat widget) at the cost of more server traffic. 15 s triples server load vs. the 60 s default — use sparingly. 30 s and 45 s require a page reload to apply exactly; 15 s and 60 s take effect immediately."
4267 )}
4268 </p>
4269 </div>
4270 <div class="desktop-mode-features__row">
4271 <wpd-button
4272 variant="secondary"
4273 ?disabled=${resetting}
4274 @click=${onResetIntros}
4275 >
4276 ${resetting ? __("Resetting…") : __("Reset what’s-new dialogs")}
4277 </wpd-button>
4278 <p class="desktop-mode-features__hint">
4279 ${__(
4280 "Re-shows the one-time introduction dialog the next time you open each redesigned native window."
4281 )}
4282 </p>
4283 </div>
4284 </wpd-section>
4285 `,
4286 wrapper
4287 );
4288 paint();
4289 return wrapper;
4290 }
4291 const WPD_COMPONENT_TAGS = [
4292 "wpd-section",
4293 "wpd-button",
4294 "wpd-swatch",
4295 "wpd-swatch-grid",
4296 "wpd-segmented",
4297 "wpd-segment",
4298 "wpd-select",
4299 "wpd-option",
4300 "wpd-multiselect",
4301 "wpd-color-field",
4302 "wpd-range-field",
4303 "wpd-text-field",
4304 "wpd-number-field",
4305 "wpd-checkbox",
4306 "wpd-checkbox-label",
4307 "wpd-toast",
4308 "wpd-toast-container",
4309 "wpd-tabs",
4310 "wpd-tab",
4311 "wpd-tabpanel",
4312 "wpd-window-button",
4313 "wpd-menu",
4314 "wpd-menu-item",
4315 "wpd-context-menu",
4316 "wpd-context-menu-option",
4317 "wpd-confirm-dialog",
4318 "wpd-modal",
4319 "wpd-user-search",
4320 "wpd-role-picker",
4321 "wpd-flyout",
4322 "wpd-tab-chip",
4323 "wpd-stack",
4324 "wpd-cluster",
4325 "wpd-icon",
4326 "wpd-body",
4327 "wpd-panel",
4328 "wpd-row",
4329 "wpd-grid",
4330 "wpd-display",
4331 "wpd-empty-state",
4332 "wpd-key",
4333 "wpd-code",
4334 "wpd-badge",
4335 "wpd-log",
4336 "wpd-steps",
4337 "wpd-step",
4338 "wpd-table",
4339 "wpd-spinner",
4340 "wpd-relative-time",
4341 "wpd-avatar",
4342 "wpd-textarea",
4343 "wpd-chip",
4344 "wpd-tag-input",
4345 "wpd-form",
4346 "wpd-save-status",
4347 "wpd-category-picker",
4348 "wpd-crumb-chain",
4349 "wpd-card",
4350 "wpd-notice"
4351 ];
4352 let demoBannerLogged = false;
4353 function logDemoBanner() {
4354 if (demoBannerLogged) {
4355 return;
4356 }
4357 demoBannerLogged = true;
4358 const headingStyle = [
4359 "background: #ffb400",
4360 "color: #1a1a1a",
4361 "font-weight: 700",
4362 "font-size: 12px",
4363 "padding: 4px 8px",
4364 "border-radius: 3px"
4365 ].join(";");
4366 const bodyStyle = [
4367 "color: #b25c00",
4368 "font-weight: 500"
4369 ].join(";");
4370 console.log(
4371 '%c⚠ wp.desktop — INTENTIONAL DEMO%c\nThe next three console.error entries are fired ON PURPOSE by the\nOS Settings → Components tab to demonstrate the <wpd-*> missing-\nimport warner. They are not real bugs.\n\n 1. <wpd-example-console-fail-due-to-unregistered-component>\n 2. <wpd-buton> (typo of <wpd-button>)\n 3. <wpd-totally-made-up-thing>\n\nSource: src/settings/sections/help.ts — the "Missing-import\nwarner — live demo" section. Remove that section in your fork\nif you want a quieter Components tab.',
4372 headingStyle,
4373 bodyStyle
4374 );
4375 }
4376 function buildHelpSection() {
4377 const entries = collectEntries();
4378 const el = document.createElement("div");
4379 el.classList.add("desktop-mode-os-settings__help");
4380 logDemoBanner();
4381 let activeTag = entries[0]?.tag ?? "";
4382 const paint = () => {
4383 const active = entries.find((e) => e.tag === activeTag) ?? entries[0];
4384 render(
4385 html`
4386 <wpd-section
4387 heading=${__("Component library")}
4388 description=${__(
4389 "Every <wpd-*> web component shipped by this plugin, with its props, slots, and a live example. Descriptors live next to each component class — the list stays in sync with the code."
4390 )}
4391 >
4392 <p class="desktop-mode-os-settings__help-count">
4393 ${String(entries.length)} ${__("components registered.")}
4394 </p>
4395 </wpd-section>
4396
4397 <wpd-section
4398 heading=${__("Missing-import warner — live demo")}
4399 description=${__(
4400 'The three <wpd-*> tags below are intentionally bogus. Open the browser console: within ~2 seconds you should see three console.error entries from the framework, each pointing the developer at the fix (typo with "did you mean", and unknown tags). The tags are kept off-screen so they do not affect layout. Remove this section in your fork if you want a quieter Components tab.'
4401 )}
4402 >
4403 <div
4404 class="desktop-mode-os-settings__help-warner-demo"
4405 aria-hidden="true"
4406 style="position:absolute;width:0;height:0;overflow:hidden;clip:rect(0 0 0 0);"
4407 >
4408 <!--
4409 Case 1 — invented name, nothing close in the registry.
4410 Triggers the "no component by that name exists" branch.
4411 -->
4412 <wpd-example-console-fail-due-to-unregistered-component></wpd-example-console-fail-due-to-unregistered-component>
4413
4414 <!--
4415 Case 2 — typo within Levenshtein distance of a real tag.
4416 Triggers the "Did you mean <wpd-button>?" branch.
4417 -->
4418 <wpd-buton></wpd-buton>
4419
4420 <!--
4421 Case 3 — looks plausible but is not in the registry.
4422 Triggers the unknown-tag branch with no suggestion.
4423 -->
4424 <wpd-totally-made-up-thing></wpd-totally-made-up-thing>
4425 </div>
4426 </wpd-section>
4427
4428 <div class="desktop-mode-os-settings__help-layout">
4429 <nav
4430 class="desktop-mode-os-settings__help-nav"
4431 aria-label=${__("Components")}
4432 >
4433 ${entries.map(
4434 (entry) => html`
4435 <button
4436 type="button"
4437 class=${classNames(
4438 "desktop-mode-os-settings__help-nav-item",
4439 entry.tag === (active?.tag ?? "") ? "is-active" : ""
4440 )}
4441 aria-pressed=${entry.tag === (active?.tag ?? "") ? "true" : "false"}
4442 @click=${() => {
4443 activeTag = entry.tag;
4444 paint();
4445 }}
4446 >
4447 <span class="desktop-mode-os-settings__help-nav-title"
4448 >${entry.title}</span
4449 >
4450 <span class="desktop-mode-os-settings__help-nav-tag"
4451 >&lt;${entry.tag}&gt;</span
4452 >
4453 </button>
4454 `
4455 )}
4456 </nav>
4457 <div class="desktop-mode-os-settings__help-detail">
4458 ${active ? renderDetail(active) : renderEmpty()}
4459 </div>
4460 </div>
4461 `,
4462 el
4463 );
4464 };
4465 paint();
4466 return el;
4467 }
4468 function renderDetail(entry) {
4469 const help = entry.help;
4470 const status = help?.status ?? "stable";
4471 const since = help?.since;
4472 return html`
4473 <header class="desktop-mode-os-settings__help-head">
4474 <h3 class="desktop-mode-os-settings__help-title">${entry.title}</h3>
4475 <code class="desktop-mode-os-settings__help-code"
4476 >&lt;${entry.tag}&gt;</code
4477 >
4478 <span
4479 class=${classNames(
4480 "desktop-mode-os-settings__help-badge",
4481 `is-${status}`
4482 )}
4483 >${statusLabel(status)}</span
4484 >
4485 ${since ? html`<span class="desktop-mode-os-settings__help-since"
4486 >${__("Since")} ${since}</span
4487 >` : html``}
4488 </header>
4489
4490 ${help?.summary ? html`<p class="desktop-mode-os-settings__help-summary">
4491 ${help.summary}
4492 </p>` : html``}
4493
4494 ${help?.example ? html`
4495 <section class="desktop-mode-os-settings__help-group">
4496 <h4>${__("Example")}</h4>
4497 <div class="desktop-mode-os-settings__help-example">
4498 ${help.example}
4499 </div>
4500 </section>
4501 ` : html``}
4502 ${renderPropsTable(entry, help)} ${renderSlots(help)}
4503 ${renderEvents(help)} ${renderParts(help)}
4504 ${renderCssProps(help)}
4505 ${!help ? html`<p class="desktop-mode-os-settings__help-note">
4506 ${__(
4507 "This component has no help descriptor yet. Add `static help` to its class for a fuller reference."
4508 )}
4509 </p>` : html``}
4510 `;
4511 }
4512 function renderPropsTable(entry, help) {
4513 const documented = help?.props ?? [];
4514 const documentedNames = new Set(documented.map((p) => p.name));
4515 const undocumented = entry.props.filter((p) => !documentedNames.has(p));
4516 if (documented.length === 0 && undocumented.length === 0) {
4517 return html``;
4518 }
4519 return html`
4520 <section class="desktop-mode-os-settings__help-group">
4521 <h4>${__("Props")}</h4>
4522 <table class="desktop-mode-os-settings__help-table">
4523 <thead>
4524 <tr>
4525 <th>${__("Name")}</th>
4526 <th>${__("Type")}</th>
4527 <th>${__("Default")}</th>
4528 <th>${__("Description")}</th>
4529 </tr>
4530 </thead>
4531 <tbody>
4532 ${documented.map(
4533 (p) => html`
4534 <tr>
4535 <td><code>${p.name}</code></td>
4536 <td>${p.type ?? "—"}</td>
4537 <td>${p.default ?? "—"}</td>
4538 <td>${p.description ?? ""}</td>
4539 </tr>
4540 `
4541 )}
4542 ${undocumented.map(
4543 (name) => html`
4544 <tr>
4545 <td><code>${name}</code></td>
4546 <td>—</td>
4547 <td>—</td>
4548 <td>
4549 <em
4550 >${__(
4551 "Undocumented — declared via static props."
4552 )}</em
4553 >
4554 </td>
4555 </tr>
4556 `
4557 )}
4558 </tbody>
4559 </table>
4560 </section>
4561 `;
4562 }
4563 function renderSlots(help) {
4564 if (!help?.slots?.length) {
4565 return html``;
4566 }
4567 return html`
4568 <section class="desktop-mode-os-settings__help-group">
4569 <h4>${__("Slots")}</h4>
4570 <ul class="desktop-mode-os-settings__help-list">
4571 ${help.slots.map(
4572 (s) => html`
4573 <li>
4574 <code>${s.name}</code>
4575 ${s.description ? html` — ${s.description}` : html``}
4576 </li>
4577 `
4578 )}
4579 </ul>
4580 </section>
4581 `;
4582 }
4583 function renderEvents(help) {
4584 if (!help?.events?.length) {
4585 return html``;
4586 }
4587 return html`
4588 <section class="desktop-mode-os-settings__help-group">
4589 <h4>${__("Events")}</h4>
4590 <ul class="desktop-mode-os-settings__help-list">
4591 ${help.events.map(
4592 (e) => html`
4593 <li>
4594 <code>${e.name}</code>
4595 ${e.detail ? html` — <code>${e.detail}</code>` : html``}
4596 ${e.description ? html` — ${e.description}` : html``}
4597 </li>
4598 `
4599 )}
4600 </ul>
4601 </section>
4602 `;
4603 }
4604 function renderParts(help) {
4605 if (!help?.parts?.length) {
4606 return html``;
4607 }
4608 return html`
4609 <section class="desktop-mode-os-settings__help-group">
4610 <h4>${__("Shadow parts")}</h4>
4611 <ul class="desktop-mode-os-settings__help-list">
4612 ${help.parts.map(
4613 (p) => html`
4614 <li>
4615 <code>::part(${p.name})</code>
4616 ${p.description ? html` — ${p.description}` : html``}
4617 </li>
4618 `
4619 )}
4620 </ul>
4621 </section>
4622 `;
4623 }
4624 function renderCssProps(help) {
4625 if (!help?.cssProps?.length) {
4626 return html``;
4627 }
4628 return html`
4629 <section class="desktop-mode-os-settings__help-group">
4630 <h4>${__("CSS custom properties")}</h4>
4631 <ul class="desktop-mode-os-settings__help-list">
4632 ${help.cssProps.map(
4633 (v) => html`
4634 <li>
4635 <code>${v.name}</code>
4636 ${v.default ? html`
4637 (${__("default")}
4638 <code>${v.default}</code>)
4639 ` : html``}
4640 ${v.description ? html` — ${v.description}` : html``}
4641 </li>
4642 `
4643 )}
4644 </ul>
4645 </section>
4646 `;
4647 }
4648 function renderEmpty() {
4649 return html`<p>${__("No components registered.")}</p>`;
4650 }
4651 function collectEntries() {
4652 const entries = [];
4653 for (const tag of WPD_COMPONENT_TAGS) {
4654 const ctor = customElements.get(tag);
4655 if (!ctor) {
4656 continue;
4657 }
4658 const help = ctor.help ?? null;
4659 const title = help?.title ?? defaultTitleFromTag(tag);
4660 const props = ctor.props ?? [];
4661 entries.push({ tag, title, help, props });
4662 }
4663 entries.sort((a, b) => a.title.localeCompare(b.title));
4664 return entries;
4665 }
4666 function defaultTitleFromTag(tag) {
4667 const bare = tag.replace(/^wpd-/, "").replace(/-/g, " ");
4668 return bare.charAt(0).toUpperCase() + bare.slice(1);
4669 }
4670 function statusLabel(status) {
4671 switch (status) {
4672 case "experimental":
4673 return __("Experimental");
4674 case "planned":
4675 return __("Planned");
4676 case "stable":
4677 default:
4678 return __("Stable");
4679 }
4680 }
4681 function classNames(...parts) {
4682 return parts.filter(Boolean).join(" ");
4683 }
4684 function collectRegistrationErrors(def, checks) {
4685 if (!def || typeof def !== "object") {
4686 return ["def (not an object)"];
4687 }
4688 const d = def;
4689 const errors = [];
4690 for (const check of checks) {
4691 if (!check.valid(d)) {
4692 errors.push(`${check.field} (${check.message})`);
4693 }
4694 }
4695 return errors;
4696 }
4697 class RegistrationError extends Error {
4698 constructor(kind, errors, def) {
4699 super(
4700 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + "."
4701 );
4702 this.name = "RegistrationError";
4703 this.kind = kind;
4704 this.errors = errors;
4705 this.def = def;
4706 }
4707 }
4708 function throwOnRegistrationErrors(kind, errors, def) {
4709 if (errors.length === 0) {
4710 return;
4711 }
4712 throw new RegistrationError(kind, errors, def);
4713 }
4714 const store = createSharedStore(
4715 "desktop-mode/wallpaper-registry",
4716 () => ({
4717 seed: [],
4718 listeners: /* @__PURE__ */ new Set()
4719 })
4720 );
4721 const seed = store.state.seed;
4722 const listeners = store.state.listeners;
4723 function register(def) {
4724 throwOnRegistrationErrors(
4725 "Wallpaper",
4726 collectRegistrationErrors(def, WALLPAPER_CHECKS),
4727 def
4728 );
4729 const idx = seed.findIndex((w) => w.id === def.id);
4730 if (idx >= 0) {
4731 seed[idx] = def;
4732 } else {
4733 seed.push(def);
4734 }
4735 notify();
4736 }
4737 function unregister(id) {
4738 const idx = seed.findIndex((w) => w.id === id);
4739 if (idx >= 0) {
4740 seed.splice(idx, 1);
4741 notify();
4742 }
4743 }
4744 function subscribe(cb) {
4745 listeners.add(cb);
4746 return () => {
4747 listeners.delete(cb);
4748 };
4749 }
4750 function notify() {
4751 const snapshot = Array.from(listeners);
4752 for (const cb of snapshot) {
4753 try {
4754 cb();
4755 } catch (err) {
4756 if (typeof console !== "undefined") {
4757 console.error(
4758 "[desktop-mode] wallpaper registry listener threw:",
4759 err
4760 );
4761 }
4762 }
4763 }
4764 }
4765 function all() {
4766 const copy = seed.slice();
4767 const filtered = applyFilters(HOOKS.WALLPAPERS, copy);
4768 if (!Array.isArray(filtered)) {
4769 if (typeof console !== "undefined") {
4770 console.warn(
4771 "[desktop-mode] `desktop-mode.wallpapers` filter returned a non-array; falling back to seed list."
4772 );
4773 }
4774 return copy;
4775 }
4776 return filtered.filter(isValidDef);
4777 }
4778 function get(id) {
4779 return all().find((w) => w.id === id);
4780 }
4781 const WALLPAPER_CHECKS = [
4782 {
4783 field: "id",
4784 message: "missing or not a non-empty string",
4785 valid: (d) => typeof d.id === "string" && d.id !== ""
4786 },
4787 {
4788 field: "label",
4789 message: "missing or not a non-empty string",
4790 valid: (d) => typeof d.label === "string" && d.label !== ""
4791 },
4792 {
4793 field: "preview",
4794 message: "missing or not a non-empty string",
4795 valid: (d) => typeof d.preview === "string" && d.preview !== ""
4796 },
4797 {
4798 field: "type",
4799 message: 'must be "css" or "canvas"',
4800 valid: (d) => d.type === "css" || d.type === "canvas"
4801 },
4802 {
4803 field: "value/resolveValue/mount",
4804 message: "css types need `value` or `resolveValue`; canvas types need `mount`",
4805 valid: (d) => {
4806 if (d.type === "css") {
4807 return typeof d.value === "string" || typeof d.resolveValue === "function";
4808 }
4809 if (d.type === "canvas") {
4810 return typeof d.mount === "function";
4811 }
4812 return true;
4813 }
4814 }
4815 ];
4816 function isValidDef(def) {
4817 return collectRegistrationErrors(def, WALLPAPER_CHECKS).length === 0;
4818 }
4819 function isPromise(value) {
4820 return !!value && typeof value === "object" && typeof value.then === "function";
4821 }
4822 function sanitizeFilename(name) {
4823 const cleaned = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4824 return cleaned || "wallpaper";
4825 }
4826 function isUsableImage(item) {
4827 if (!item || typeof item.id !== "number" || !item.source_url) {
4828 return false;
4829 }
4830 const d = item.media_details;
4831 return !!d && typeof d.width === "number" && typeof d.height === "number" && d.width > 0 && d.height > 0;
4832 }
4833 function stripHtml(markup) {
4834 if (!markup) {
4835 return "";
4836 }
4837 const el = document.createElement("div");
4838 el.innerHTML = markup;
4839 return el.textContent?.trim() || "";
4840 }
4841 async function fetchMediaPage(config, page, search, hdOnly) {
4842 const url = new URL(config.mediaUrl);
4843 url.searchParams.set("media_type", "image");
4844 url.searchParams.set("per_page", String(MEDIA_PER_PAGE));
4845 url.searchParams.set("page", String(page));
4846 url.searchParams.set("orderby", "date");
4847 url.searchParams.set("order", "desc");
4848 url.searchParams.set(
4849 "_fields",
4850 "id,source_url,alt_text,title,media_details"
4851 );
4852 if (search) {
4853 url.searchParams.set("search", search);
4854 }
4855 if (hdOnly) {
4856 url.searchParams.set("desktop_mode_min_width", String(HD_MIN_WIDTH));
4857 url.searchParams.set("desktop_mode_min_height", String(HD_MIN_HEIGHT));
4858 }
4859 const response = await trackedFetch(
4860 url.toString(),
4861 {
4862 credentials: "same-origin",
4863 headers: { "X-WP-Nonce": config.restNonce }
4864 },
4865 { source: "desktop-mode/settings/media" }
4866 );
4867 if (!response.ok) {
4868 let message = `HTTP ${response.status}`;
4869 try {
4870 const data = await response.json();
4871 if (data && typeof data.message === "string") {
4872 message = data.message;
4873 }
4874 } catch {
4875 }
4876 throw new Error(message);
4877 }
4878 const totalPagesHeader = response.headers.get("X-WP-TotalPages");
4879 const totalPages = totalPagesHeader ? parseInt(totalPagesHeader, 10) : 1;
4880 const items = await response.json();
4881 return { items: items.filter(isUsableImage), totalPages: totalPages || 1 };
4882 }
4883 async function uploadImage(config, file) {
4884 const response = await trackedFetch(
4885 config.mediaUrl,
4886 {
4887 method: "POST",
4888 credentials: "same-origin",
4889 headers: {
4890 "X-WP-Nonce": config.restNonce,
4891 "Content-Type": file.type,
4892 "Content-Disposition": `attachment; filename="${sanitizeFilename(file.name)}"`
4893 },
4894 body: file
4895 },
4896 { source: "desktop-mode/settings/media-upload" }
4897 );
4898 if (!response.ok) {
4899 let message = `Upload failed (HTTP ${response.status}).`;
4900 try {
4901 const data2 = await response.json();
4902 if (data2 && typeof data2.message === "string") {
4903 message = data2.message;
4904 }
4905 } catch {
4906 }
4907 throw new Error(message);
4908 }
4909 const data = await response.json();
4910 return { id: data.id, url: data.source_url };
4911 }
4912 function buildCustomImageSection(ctx, body) {
4913 const tabDefs = [];
4914 const pane = document.createElement("div");
4915 pane.className = "desktop-mode-os-settings__tab-pane";
4916 if (ctx.config.canUpload) {
4917 tabDefs.push({
4918 key: "upload",
4919 label: __("Upload new"),
4920 render: () => renderUploadPane(ctx, pane, body)
4921 });
4922 }
4923 tabDefs.push({
4924 key: "library",
4925 label: __("Media Library"),
4926 render: () => renderLibraryPane(ctx, pane, body)
4927 });
4928 const initialKey = tabDefs[0].key;
4929 const onTabChange = (e) => {
4930 const key = e.detail.value;
4931 tabDefs.find((t) => t.key === key)?.render();
4932 };
4933 const wrap = document.createElement("div");
4934 render(
4935 html`
4936 <div class="desktop-mode-os-settings__uploader">
4937 <h4 class="desktop-mode-os-settings__uploader-heading">
4938 ${__("Or use your own image")}
4939 </h4>
4940 ${tabDefs.length > 1 ? html`<wpd-tabs
4941 value=${initialKey}
4942 label=${__("Image source")}
4943 @wpd-tab-change=${onTabChange}
4944 >
4945 ${tabDefs.map(
4946 (def) => html`<wpd-tab value=${def.key}
4947 >${def.label}</wpd-tab
4948 >`
4949 )}
4950 </wpd-tabs>` : null}
4951 ${pane}
4952 </div>
4953 `,
4954 wrap
4955 );
4956 tabDefs.find((t) => t.key === initialKey)?.render();
4957 return wrap.firstElementChild;
4958 }
4959 function renderUploadPane(ctx, pane, body) {
4960 const tile = document.createElement("div");
4961 tile.className = "desktop-mode-os-settings__upload-tile";
4962 tile.dataset.wallpaperId = CUSTOM_IMAGE_ID;
4963 tile.setAttribute(
4964 "aria-pressed",
4965 ctx.state.wallpaper === CUSTOM_IMAGE_ID ? "true" : "false"
4966 );
4967 const fileInput = document.createElement("input");
4968 fileInput.type = "file";
4969 fileInput.accept = "image/*";
4970 fileInput.className = "desktop-mode-os-settings__file-input";
4971 fileInput.addEventListener("change", () => {
4972 const file = fileInput.files?.[0];
4973 if (file) {
4974 void handleImageFile(ctx, file, tile, body);
4975 }
4976 fileInput.value = "";
4977 });
4978 render(html`${fileInput}${tile}`, pane);
4979 renderUploadTile(ctx, tile, fileInput, body);
4980 }
4981 function renderUploadTile(ctx, tile, fileInput, body) {
4982 tile.classList.remove("desktop-mode-os-settings__upload-tile--filled");
4983 tile.classList.remove("desktop-mode-os-settings__upload-tile--dragover");
4984 tile.classList.remove("desktop-mode-os-settings__upload-tile--busy");
4985 tile.removeAttribute("aria-label");
4986 const hasImage = !!ctx.state.customImage;
4987 if (hasImage) {
4988 tile.classList.add("desktop-mode-os-settings__upload-tile--filled");
4989 tile.setAttribute("aria-label", __("Custom image wallpaper"));
4990 tile.style.backgroundImage = `url("${encodeURI(ctx.state.customImage.url)}")`;
4991 } else {
4992 tile.style.backgroundImage = "";
4993 tile.setAttribute("aria-label", __("Upload a wallpaper image"));
4994 }
4995 const onRemove = (e) => {
4996 e.stopPropagation();
4997 ctx.state.customImage = null;
4998 if (ctx.state.wallpaper === CUSTOM_IMAGE_ID) {
4999 ctx.state.wallpaper = DEFAULT_WALLPAPER_ID;
5000 }
5001 registerCustomImageIfPresent(ctx.state);
5002 ctx.save();
5003 ctx.apply();
5004 renderUploadTile(ctx, tile, fileInput, body);
5005 refreshWallpaperPressedState(ctx, body);
5006 };
5007 render(
5008 hasImage ? html`
5009 <wpd-button
5010 variant="danger"
5011 class="desktop-mode-os-settings__upload-remove"
5012 aria-label=${__("Remove custom image")}
5013 @click=${onRemove}
5014 >${__("Remove")}</wpd-button
5015 >
5016 ` : html`
5017 <div class="desktop-mode-os-settings__upload-inner">
5018 <span
5019 class="desktop-mode-os-settings__upload-plus"
5020 aria-hidden="true"
5021 >+</span
5022 >
5023 <span class="desktop-mode-os-settings__upload-prompt"
5024 >${__("Drop an image here, or click to upload")}</span
5025 >
5026 <span class="desktop-mode-os-settings__upload-hint"
5027 >${__(
5028 "JPEG, PNG, or WebP · goes straight to your Media Library"
5029 )}</span
5030 >
5031 </div>
5032 `,
5033 tile
5034 );
5035 tile.onclick = () => {
5036 if (tile.classList.contains("desktop-mode-os-settings__upload-tile--busy")) {
5037 return;
5038 }
5039 if (ctx.state.customImage) {
5040 selectWallpaper(ctx, CUSTOM_IMAGE_ID, body);
5041 return;
5042 }
5043 fileInput.click();
5044 };
5045 tile.ondragover = (e) => {
5046 e.preventDefault();
5047 tile.classList.add("desktop-mode-os-settings__upload-tile--dragover");
5048 };
5049 tile.ondragleave = () => {
5050 tile.classList.remove("desktop-mode-os-settings__upload-tile--dragover");
5051 };
5052 tile.ondrop = (e) => {
5053 e.preventDefault();
5054 tile.classList.remove("desktop-mode-os-settings__upload-tile--dragover");
5055 const file = e.dataTransfer?.files?.[0];
5056 if (file) {
5057 void handleImageFile(ctx, file, tile, body);
5058 }
5059 };
5060 }
5061 async function handleImageFile(ctx, file, tile, body) {
5062 if (!file.type.startsWith("image/")) {
5063 showUploadError(tile, __("That file isn’t an image."));
5064 return;
5065 }
5066 tile.classList.add("desktop-mode-os-settings__upload-tile--busy");
5067 render(
5068 html`<span class="desktop-mode-os-settings__upload-status"
5069 >${__("Uploading…")}</span
5070 >`,
5071 tile
5072 );
5073 const fileInput = tile.parentElement?.querySelector(
5074 ".desktop-mode-os-settings__file-input"
5075 );
5076 try {
5077 const media = await uploadImage(ctx.config, file);
5078 ctx.state.customImage = { id: media.id, url: media.url };
5079 ctx.state.wallpaper = CUSTOM_IMAGE_ID;
5080 registerCustomImageIfPresent(ctx.state);
5081 ctx.save();
5082 ctx.apply();
5083 if (fileInput) {
5084 renderUploadTile(ctx, tile, fileInput, body);
5085 }
5086 refreshWallpaperPressedState(ctx, body);
5087 } catch (err) {
5088 tile.classList.remove("desktop-mode-os-settings__upload-tile--busy");
5089 if (fileInput) {
5090 renderUploadTile(ctx, tile, fileInput, body);
5091 }
5092 const message = err instanceof Error ? err.message : __("Upload failed.");
5093 showUploadError(tile, message);
5094 }
5095 }
5096 function showUploadError(tile, message) {
5097 let err = tile.querySelector(".desktop-mode-os-settings__upload-error");
5098 if (!err) {
5099 err = document.createElement("span");
5100 err.className = "desktop-mode-os-settings__upload-error";
5101 err.setAttribute("role", "status");
5102 tile.appendChild(err);
5103 }
5104 err.textContent = message;
5105 window.setTimeout(() => {
5106 err?.remove();
5107 }, 4e3);
5108 }
5109 function renderLibraryPane(ctx, pane, body) {
5110 const search = document.createElement("input");
5111 search.type = "search";
5112 search.placeholder = __("Search your media");
5113 search.className = "desktop-mode-os-settings__library-search";
5114 search.setAttribute("aria-label", __("Search media"));
5115 const grid = document.createElement("div");
5116 grid.className = "desktop-mode-os-settings__library-grid";
5117 const meta = document.createElement("span");
5118 meta.className = "desktop-mode-os-settings__library-meta";
5119 const loadMore = document.createElement("wpd-button");
5120 loadMore.setAttribute("variant", "ghost");
5121 loadMore.textContent = __("Load more");
5122 let query = "";
5123 let page = 0;
5124 let totalPages = 0;
5125 let loaded = [];
5126 let hiddenByHd = 0;
5127 let loading = false;
5128 const onHdToggle = (e) => {
5129 ctx.state.libraryHdOnly = e.detail.checked;
5130 ctx.save();
5131 resetAndReload();
5132 };
5133 render(
5134 html`
5135 <div class="desktop-mode-os-settings__library">
5136 <div class="desktop-mode-os-settings__library-toolbar">
5137 ${search}
5138 <wpd-checkbox-label
5139 label=${sprintf(
5140 // translators: %1$d is the HD minimum width in px, %2$d is the minimum height.
5141 __("Only HD (≥%1$d×%2$d)"),
5142 HD_MIN_WIDTH,
5143 HD_MIN_HEIGHT
5144 )}
5145 ?checked=${ctx.state.libraryHdOnly}
5146 @wpd-checkbox-change=${onHdToggle}
5147 ></wpd-checkbox-label>
5148 </div>
5149 ${grid}
5150 <div class="desktop-mode-os-settings__library-footer">
5151 ${meta}${loadMore}
5152 </div>
5153 </div>
5154 `,
5155 pane
5156 );
5157 const updateMeta = () => {
5158 const visible = visibleLibraryItems(ctx.state, loaded).length;
5159 const parts = [
5160 // translators: %d is the number of media items currently visible.
5161 sprintf(__("Showing %d"), visible)
5162 ];
5163 if (ctx.state.libraryHdOnly && hiddenByHd > 0) {
5164 parts.push(
5165 // translators: %d is the number of images filtered out by the HD toggle.
5166 sprintf(__("%d hidden by HD filter"), hiddenByHd)
5167 );
5168 }
5169 meta.textContent = parts.join(" · ");
5170 loadMore.hidden = page >= totalPages;
5171 if (loading) {
5172 loadMore.setAttribute("disabled", "");
5173 } else {
5174 loadMore.removeAttribute("disabled");
5175 }
5176 };
5177 const renderGrid = () => {
5178 const visible = visibleLibraryItems(ctx.state, loaded);
5179 hiddenByHd = loaded.length - visible.length;
5180 if (visible.length === 0 && !loading) {
5181 render(
5182 html`<p class="desktop-mode-os-settings__library-empty">
5183 ${ctx.state.libraryHdOnly ? __(
5184 "No HD images found. Try unchecking the filter, or upload a larger image."
5185 ) : __("No images in your Media Library yet.")}
5186 </p>`,
5187 grid
5188 );
5189 } else {
5190 grid.innerHTML = "";
5191 for (const item of visible) {
5192 grid.appendChild(buildLibraryTile(ctx, item, body));
5193 }
5194 }
5195 updateMeta();
5196 };
5197 const loadNextPage = async () => {
5198 if (loading || totalPages > 0 && page >= totalPages) {
5199 return;
5200 }
5201 loading = true;
5202 updateMeta();
5203 if (page === 0) {
5204 render(
5205 html`${Array.from(
5206 { length: 8 },
5207 () => html`<div
5208 class="desktop-mode-os-settings__library-tile desktop-mode-os-settings__library-tile--skeleton"
5209 ></div>`
5210 )}`,
5211 grid
5212 );
5213 }
5214 try {
5215 const result = await fetchMediaPage(
5216 ctx.config,
5217 page + 1,
5218 query,
5219 ctx.state.libraryHdOnly
5220 );
5221 page = page + 1;
5222 totalPages = result.totalPages;
5223 loaded = loaded.concat(result.items);
5224 renderGrid();
5225 } catch (err) {
5226 render(
5227 html`<p class="desktop-mode-os-settings__library-error">
5228 ${err instanceof Error ? sprintf(
5229 // translators: %s is the browser-supplied error message.
5230 __("Couldn’t load your media: %s"),
5231 err.message
5232 ) : __("Couldn’t load your media.")}
5233 </p>`,
5234 grid
5235 );
5236 } finally {
5237 loading = false;
5238 updateMeta();
5239 }
5240 };
5241 const resetAndReload = () => {
5242 page = 0;
5243 totalPages = 0;
5244 loaded = [];
5245 hiddenByHd = 0;
5246 void loadNextPage();
5247 };
5248 let searchTimer = null;
5249 search.addEventListener("input", () => {
5250 if (searchTimer !== null) {
5251 window.clearTimeout(searchTimer);
5252 }
5253 searchTimer = window.setTimeout(() => {
5254 searchTimer = null;
5255 query = search.value.trim();
5256 resetAndReload();
5257 }, SEARCH_DEBOUNCE_MS);
5258 });
5259 loadMore.addEventListener("click", () => {
5260 void loadNextPage();
5261 });
5262 void loadNextPage();
5263 }
5264 function visibleLibraryItems(state, items) {
5265 if (!state.libraryHdOnly) {
5266 return items;
5267 }
5268 return items.filter(
5269 (it) => it.media_details.width >= HD_MIN_WIDTH && it.media_details.height >= HD_MIN_HEIGHT
5270 );
5271 }
5272 function buildLibraryTile(ctx, item, body) {
5273 const isSelected = ctx.state.wallpaper === CUSTOM_IMAGE_ID && ctx.state.customImage?.id === item.id;
5274 const sizes = item.media_details.sizes || {};
5275 const thumbUrl = sizes.medium?.source_url || sizes.thumbnail?.source_url || sizes.large?.source_url || item.source_url;
5276 const altOrTitle = item.alt_text || stripHtml(item.title?.rendered || "") || `Image #${item.id}`;
5277 const onClick = () => {
5278 ctx.state.customImage = { id: item.id, url: item.source_url };
5279 ctx.state.wallpaper = CUSTOM_IMAGE_ID;
5280 registerCustomImageIfPresent(ctx.state);
5281 ctx.save();
5282 ctx.apply();
5283 refreshWallpaperPressedState(ctx, body);
5284 const tileGrid = wrapper.firstElementChild?.parentElement;
5285 if (tileGrid) {
5286 tileGrid.querySelectorAll("[data-media-id]").forEach((el) => {
5287 const selected = el.dataset.mediaId === String(item.id);
5288 el.setAttribute("aria-pressed", selected ? "true" : "false");
5289 el.classList.toggle(
5290 "desktop-mode-os-settings__library-tile--selected",
5291 selected
5292 );
5293 });
5294 }
5295 };
5296 const wrapper = document.createElement("div");
5297 render(
5298 html`
5299 <button
5300 type="button"
5301 class=${isSelected ? "desktop-mode-os-settings__library-tile desktop-mode-os-settings__library-tile--selected" : "desktop-mode-os-settings__library-tile"}
5302 data-media-id=${String(item.id)}
5303 aria-pressed=${isSelected ? "true" : "false"}
5304 aria-label=${altOrTitle}
5305 title=${altOrTitle}
5306 style=${`background-image: url("${encodeURI(thumbUrl)}")`}
5307 @click=${onClick}
5308 >
5309 <span class="desktop-mode-os-settings__library-tile-dims"
5310 >${item.media_details.width}×${item.media_details.height}</span
5311 >
5312 </button>
5313 `,
5314 wrapper
5315 );
5316 return wrapper.firstElementChild;
5317 }
5318 function customGradientCss(state) {
5319 const { from, to, angle } = state.customGradient;
5320 return `linear-gradient(${angle}deg, ${from}, ${to})`;
5321 }
5322 function attachCustomGradientEditor(ctx) {
5323 register({
5324 id: CUSTOM_GRADIENT_ID,
5325 label: __("Custom gradient"),
5326 type: "css",
5327 preview: customGradientCss(ctx.state),
5328 resolveValue: () => customGradientCss(ctx.state),
5329 renderEditor: (container) => renderCustomGradientEditor(ctx, container)
5330 });
5331 }
5332 function registerCustomImageIfPresent(state) {
5333 if (!state.customImage) {
5334 unregister(CUSTOM_IMAGE_ID);
5335 return;
5336 }
5337 const safeUrl = encodeURI(state.customImage.url);
5338 const value = `url("${safeUrl}") center/cover no-repeat, #1d2327`;
5339 register({
5340 id: CUSTOM_IMAGE_ID,
5341 label: __("Custom image"),
5342 type: "css",
5343 value,
5344 preview: value
5345 });
5346 }
5347 function selectWallpaper(ctx, id, body) {
5348 ctx.state.wallpaper = id;
5349 ctx.save();
5350 ctx.apply();
5351 refreshWallpaperPressedState(ctx, body);
5352 }
5353 function refreshWallpaperPressedState(ctx, body) {
5354 body.querySelectorAll("[data-wallpaper-id]").forEach((el) => {
5355 const selected = el.dataset.wallpaperId === ctx.state.wallpaper;
5356 if (selected) {
5357 el.setAttribute("selected", "");
5358 } else {
5359 el.removeAttribute("selected");
5360 }
5361 el.setAttribute("aria-pressed", selected ? "true" : "false");
5362 });
5363 }
5364 function syncEditorSlot(ctx, slot, inner, def) {
5365 teardownEditor(ctx);
5366 inner.innerHTML = "";
5367 if (!def.renderEditor) {
5368 slot.dataset.expanded = "false";
5369 return;
5370 }
5371 const editorCtx = {
5372 id: def.id,
5373 pluginUrl: "",
5374 prefersReducedMotion: typeof window.matchMedia === "function" && window.matchMedia("( prefers-reduced-motion: reduce )").matches,
5375 visible: !document.hidden
5376 };
5377 try {
5378 const result = def.renderEditor(inner, editorCtx);
5379 if (isPromise(result)) {
5380 result.then((teardown) => {
5381 ctx.activeEditorTeardown = teardown;
5382 });
5383 } else {
5384 ctx.activeEditorTeardown = result;
5385 }
5386 } catch (err) {
5387 if (typeof console !== "undefined") {
5388 console.error(
5389 `[desktop-mode] Wallpaper "${def.id}" renderEditor threw:`,
5390 err
5391 );
5392 }
5393 }
5394 slot.dataset.expanded = "true";
5395 }
5396 function teardownEditor(ctx) {
5397 if (ctx.activeEditorTeardown) {
5398 try {
5399 ctx.activeEditorTeardown();
5400 } catch (err) {
5401 if (typeof console !== "undefined") {
5402 console.error(
5403 "[desktop-mode] Wallpaper editor teardown threw:",
5404 err
5405 );
5406 }
5407 }
5408 ctx.activeEditorTeardown = null;
5409 }
5410 }
5411 function renderCustomGradientEditor(ctx, container) {
5412 container.classList.add("desktop-mode-os-settings__gradient-editor-inner");
5413 const onFrom = (e) => {
5414 ctx.state.customGradient.from = e.detail.value;
5415 onChange();
5416 };
5417 const onTo = (e) => {
5418 ctx.state.customGradient.to = e.detail.value;
5419 onChange();
5420 };
5421 const onAngle = (e) => {
5422 ctx.state.customGradient.angle = e.detail.value;
5423 onChange();
5424 };
5425 const onChange = () => {
5426 ctx.save();
5427 ctx.apply();
5428 syncGradientPreviewSwatch(ctx, container);
5429 paint();
5430 };
5431 const paint = () => render(
5432 html`
5433 <div class="desktop-mode-os-settings__gradient-row">
5434 <wpd-color-field
5435 variant="block"
5436 label=${__("From")}
5437 value=${ctx.state.customGradient.from}
5438 @wpd-color-change=${onFrom}
5439 ></wpd-color-field>
5440 <wpd-color-field
5441 variant="block"
5442 label=${__("To")}
5443 value=${ctx.state.customGradient.to}
5444 @wpd-color-change=${onTo}
5445 ></wpd-color-field>
5446 </div>
5447 <wpd-range-field
5448 label=${__("Angle")}
5449 min="0"
5450 max="360"
5451 step="1"
5452 suffix="°"
5453 value=${String(ctx.state.customGradient.angle)}
5454 @wpd-range-change=${onAngle}
5455 ></wpd-range-field>
5456 `,
5457 container
5458 );
5459 paint();
5460 return () => {
5461 };
5462 }
5463 function syncGradientPreviewSwatch(ctx, editorEl) {
5464 const section = editorEl.closest("wpd-section");
5465 const preview = section?.querySelector(
5466 `[data-wallpaper-id="${CUSTOM_GRADIENT_ID}"]`
5467 );
5468 if (preview) {
5469 preview.style.background = customGradientCss(ctx.state);
5470 }
5471 }
5472 function buildWallpaperSection(ctx, body) {
5473 const editorSlot = document.createElement("div");
5474 editorSlot.className = "desktop-mode-os-settings__editor-slot";
5475 editorSlot.dataset.expanded = "false";
5476 const editorInner = document.createElement("div");
5477 editorInner.className = "desktop-mode-os-settings__editor-slot-inner";
5478 editorSlot.appendChild(editorInner);
5479 const onPick = (e) => {
5480 const id = e.detail?.value ?? "";
5481 const def = get(id);
5482 if (!def || def.id === CUSTOM_IMAGE_ID) {
5483 return;
5484 }
5485 selectWallpaper(ctx, def.id, body);
5486 syncEditorSlot(ctx, editorSlot, editorInner, def);
5487 paint();
5488 };
5489 const customImageSection = buildCustomImageSection(ctx, body);
5490 const wrapper = document.createElement("div");
5491 const paint = () => render(
5492 html`
5493 <wpd-section
5494 heading=${__("Wallpaper")}
5495 description=${__(
5496 "The backdrop behind your windows. Pick a preset, mix your own gradient, or drop in an image."
5497 )}
5498 >
5499 <div
5500 class="desktop-mode-os-settings__grid desktop-mode-os-settings__grid--wallpapers"
5501 @wpd-pick=${onPick}
5502 >
5503 ${all().filter((def) => def.id !== CUSTOM_IMAGE_ID).map(
5504 (def) => html`<wpd-swatch
5505 value=${def.id}
5506 label=${def.label}
5507 preview=${def.preview}
5508 variant="wallpaper"
5509 data-wallpaper-id=${def.id}
5510 ?selected=${ctx.state.wallpaper === def.id}
5511 >
5512 <span class="desktop-mode-os-settings__swatch-label"
5513 >${def.label}</span
5514 >
5515 </wpd-swatch>`
5516 )}
5517 </div>
5518 ${editorSlot} ${customImageSection}
5519 </wpd-section>
5520 `,
5521 wrapper
5522 );
5523 paint();
5524 const active = get(ctx.state.wallpaper);
5525 if (active) {
5526 syncEditorSlot(ctx, editorSlot, editorInner, active);
5527 }
5528 const unsubscribe = subscribe(() => {
5529 if (!wrapper.isConnected) {
5530 unsubscribe();
5531 return;
5532 }
5533 paint();
5534 const now = get(ctx.state.wallpaper);
5535 if (now) {
5536 syncEditorSlot(ctx, editorSlot, editorInner, now);
5537 }
5538 });
5539 return wrapper;
5540 }
5541 function isTabVisible(tab, isAdmin) {
5542 if (tab.capability && tab.capability === "manage_options") {
5543 return isAdmin;
5544 }
5545 return true;
5546 }
5547 function renderOsSettingsPanel(ctx, body) {
5548 attachCustomGradientEditor(ctx);
5549 teardownEditor(ctx);
5550 if (ctx.tabRegistryUnsubscribe) {
5551 ctx.tabRegistryUnsubscribe();
5552 ctx.tabRegistryUnsubscribe = null;
5553 }
5554 body.classList.add("desktop-mode-os-settings");
5555 const onReset = () => {
5556 const preservedImage = ctx.state.customImage;
5557 ctx.state = { ...DEFAULTS, customImage: preservedImage };
5558 ctx.save();
5559 ctx.apply();
5560 ctx.renderPanel(body);
5561 };
5562 const isAdmin = ctx.config.isAdmin;
5563 const externalTabs = listSettingsTabs().filter(
5564 (tab) => isTabVisible(tab, isAdmin)
5565 );
5566 const rows = [
5567 {
5568 id: "appearance",
5569 order: 10,
5570 tab: html`<wpd-tab value="appearance"
5571 >${__("Appearance")}</wpd-tab
5572 >`,
5573 panel: html`<wpd-tabpanel for="appearance">
5574 <wpd-panel>
5575 <p class="desktop-mode-os-settings__intro">
5576 ${__(
5577 "Personalize your desktop. Changes apply instantly and are saved to this browser."
5578 )}
5579 </p>
5580 ${buildWallpaperSection(ctx, body)}
5581 ${buildAccentSection(ctx)}
5582 ${buildDesktopLayoutSection(ctx)}
5583 ${buildDockSizeSection(ctx)}
5584 ${buildDockRailRendererSection(ctx)}
5585 </wpd-panel>
5586 </wpd-tabpanel>`
5587 },
5588 {
5589 id: "ai",
5590 order: 20,
5591 tab: html`<wpd-tab value="ai">${__("AI Settings")}</wpd-tab>`,
5592 panel: html`<wpd-tabpanel for="ai">
5593 <wpd-panel>${buildAiSection(ctx)}</wpd-panel>
5594 </wpd-tabpanel>`
5595 },
5596 {
5597 id: "features",
5598 order: 25,
5599 tab: html`<wpd-tab value="features"
5600 >${__("Features")}</wpd-tab
5601 >`,
5602 panel: html`<wpd-tabpanel for="features">
5603 <wpd-panel>${buildFeaturesSection(ctx)}</wpd-panel>
5604 </wpd-tabpanel>`
5605 },
5606 {
5607 id: "apps-icons",
5608 order: 22,
5609 tab: html`<wpd-tab value="apps-icons"
5610 >${__("Apps & Icons")}</wpd-tab
5611 >`,
5612 panel: html`<wpd-tabpanel for="apps-icons">
5613 <wpd-panel>${buildAppsIconsSection(ctx)}</wpd-panel>
5614 </wpd-tabpanel>`
5615 }
5616 ];
5617 if (isAdmin) {
5618 rows.push({
5619 id: "extended",
5620 order: 30,
5621 tab: html`<wpd-tab value="extended"
5622 >${__("Extended Options")}</wpd-tab
5623 >`,
5624 panel: html`<wpd-tabpanel for="extended">
5625 <wpd-panel>${buildExtendedSection(ctx)}</wpd-panel>
5626 </wpd-tabpanel>`
5627 });
5628 rows.push({
5629 id: "help",
5630 order: 40,
5631 tab: html`<wpd-tab value="help">${__("Components")}</wpd-tab>`,
5632 panel: html`<wpd-tabpanel for="help">
5633 <wpd-panel>${buildHelpSection()}</wpd-panel>
5634 </wpd-tabpanel>`
5635 });
5636 }
5637 rows.push({
5638 id: "about",
5639 order: Number.MAX_SAFE_INTEGER,
5640 tab: html`<wpd-tab value="about">${__("About")}</wpd-tab>`,
5641 panel: html`<wpd-tabpanel for="about">
5642 <wpd-panel padding="0">${buildAboutSection()}</wpd-panel>
5643 </wpd-tabpanel>`
5644 });
5645 for (const tab of externalTabs) {
5646 const tabId = `ext-${tab.id}`;
5647 const hostAttr = `wpd-settings-tab-host-${tab.id}`;
5648 const tabRef = tab;
5649 rows.push({
5650 id: tabId,
5651 order: tab.order ?? 100,
5652 tab: html`<wpd-tab value=${tabId}>${tab.label}</wpd-tab>`,
5653 panel: html`<wpd-tabpanel for=${tabId}>
5654 <wpd-panel><div data-host=${hostAttr}></div></wpd-panel>
5655 </wpd-tabpanel>`,
5656 mount: (rootBody) => {
5657 const host = rootBody.querySelector(
5658 `[data-host="${hostAttr}"]`
5659 );
5660 if (!host) {
5661 return;
5662 }
5663 try {
5664 tabRef.render(host, {
5665 isAdmin,
5666 getOsSettings: () => ctx.getOsSettingsSnapshot(),
5667 subscribeOsSettings: (cb) => ctx.subscribeOsSettings(cb)
5668 });
5669 } catch (err) {
5670 if (typeof console !== "undefined") {
5671 console.error(
5672 "[desktop-mode] settings tab render threw:",
5673 tabRef.id,
5674 err
5675 );
5676 }
5677 }
5678 }
5679 });
5680 }
5681 rows.sort((a, b) => a.order - b.order);
5682 const previousTabs = body.querySelector("wpd-tabs");
5683 const previousValue = ctx.activeTabId ?? previousTabs?.value ?? previousTabs?.getAttribute("value") ?? "appearance";
5684 const activeRowExists = rows.some((r) => r.id === previousValue);
5685 const initialTab = activeRowExists ? previousValue : "appearance";
5686 render(
5687 html`
5688 <wpd-tabs value=${initialTab} label=${__("Settings sections")}>
5689 ${rows.map((r) => r.tab)}
5690 </wpd-tabs>
5691 ${rows.map((r) => r.panel)}
5692 <wpd-panel class="desktop-mode-os-settings__footer">
5693 <wpd-button variant="ghost" @click=${onReset}
5694 >${__("Reset to defaults")}</wpd-button
5695 >
5696 </wpd-panel>
5697 `,
5698 body
5699 );
5700 for (const row of rows) {
5701 if (row.mount) {
5702 row.mount(body);
5703 }
5704 }
5705 const tabsHost = body.querySelector("wpd-tabs");
5706 if (tabsHost) {
5707 tabsHost.addEventListener("wpd-tab-change", (e) => {
5708 const detail = e.detail;
5709 if (detail?.value) {
5710 ctx.activeTabId = detail.value;
5711 }
5712 });
5713 }
5714 ctx.activeTabId = initialTab;
5715 ctx.tabRegistryUnsubscribe = subscribeSettingsTabs(() => {
5716 if (!body.isConnected) {
5717 if (ctx.tabRegistryUnsubscribe) {
5718 ctx.tabRegistryUnsubscribe();
5719 ctx.tabRegistryUnsubscribe = null;
5720 }
5721 return;
5722 }
5723 ctx.renderPanel(body);
5724 });
5725 }
5726 window.desktopModeRenderOsSettingsPanel = renderOsSettingsPanel;
5727 })();
5728