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

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

5,946 lines 194.0 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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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.5.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 unfocusEffect: "darken",
2364 customGradient: {
2365 from: "#2271b1",
2366 to: "#7c3aed",
2367 angle: 135
2368 },
2369 customImage: null,
2370 libraryHdOnly: true,
2371 ai: {
2372 enabled: false,
2373 provider: "openai",
2374 apiKey: "",
2375 apiKeys: {},
2376 transport: "off"
2377 },
2378 // Opt-IN Beta as of 0.9.1. Fresh installs land on the classic
2379 // chromeless `edit.php` iframe; a user opts in via OS Settings →
2380 // Features → Beta features to get the native Posts window. The
2381 // native windows used to default ON (opt-out, 0.8.0) but are now
2382 // opt-in so the redesign is a deliberate choice, not imposed.
2383 heartbeatRate: 60,
2384 nativePostsEnabled: false,
2385 nativePostsHiddenColumns: [],
2386 // Same opt-in Beta posture as Posts — fresh installs keep the
2387 // iframe; users opt in to the native Pages window.
2388 nativePagesEnabled: false,
2389 // Native Users window — same opt-in Beta posture. Capability-gated
2390 // server-side (the window is only registered for users with
2391 // `list_users`), so this toggle only affects the small set of
2392 // users who can see the Users tile in the first place.
2393 nativeUsersEnabled: false,
2394 // Native Plugins window — replaces `plugins.php` and
2395 // `plugin-install.php`. Same opt-in Beta posture; cap-gated on
2396 // `activate_plugins` server-side, so this toggle only affects
2397 // users who could see the Plugins tile anyway.
2398 nativePluginsEnabled: false,
2399 // Native Comments window — replaces `edit-comments.php`. Same
2400 // opt-in Beta posture; cap-gated on `edit_posts` server-side.
2401 nativeCommentsEnabled: false,
2402 showDesktopOnWallpaperClick: false,
2403 showPostStatusRibbons: true,
2404 foldersSharingEnabled: true,
2405 itemVisibility: {},
2406 dockOrder: [],
2407 dockPromotedPositions: {}
2408 };
2409 const AI_TRANSPORTS = [
2410 { id: "off", label: "Off" },
2411 { id: "sse", label: "Streaming (SSE)" }
2412 ];
2413 const AI_PROVIDERS = [
2414 {
2415 id: "openai",
2416 label: "OpenAI",
2417 apiKeyLabel: "OpenAI API key",
2418 apiKeyLink: "https://platform.openai.com/api-keys"
2419 }
2420 ];
2421 function getAiProviders() {
2422 const cfg = window.desktopModeConfig;
2423 const list2 = cfg?.aiProviders;
2424 if (!Array.isArray(list2) || list2.length === 0) {
2425 return AI_PROVIDERS;
2426 }
2427 return list2.map((p) => ({
2428 id: p.id,
2429 label: p.label,
2430 description: p.description,
2431 apiKeyLabel: p.api_key_label,
2432 apiKeyLink: p.api_key_link
2433 }));
2434 }
2435 function isPromise(value) {
2436 return !!value && typeof value === "object" && typeof value.then === "function";
2437 }
2438 function sanitizeFilename(name) {
2439 const cleaned = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
2440 return cleaned || "wallpaper";
2441 }
2442 function isUsableImage(item) {
2443 if (!item || typeof item.id !== "number" || !item.source_url) {
2444 return false;
2445 }
2446 const d = item.media_details;
2447 return !!d && typeof d.width === "number" && typeof d.height === "number" && d.width > 0 && d.height > 0;
2448 }
2449 function stripHtml(markup) {
2450 if (!markup) {
2451 return "";
2452 }
2453 const el = document.createElement("div");
2454 el.innerHTML = markup;
2455 return el.textContent?.trim() || "";
2456 }
2457 const NONCE_HEADER = "X-WP-Nonce";
2458 function injectRestNonce(input, init) {
2459 const nonce = readRestNonce();
2460 if (!nonce) {
2461 return init;
2462 }
2463 const url = resolveUrl(input);
2464 if (!url || !isSameOriginRestUrl(url)) {
2465 return init;
2466 }
2467 const baseHeaders = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
2468 const headers = new Headers(baseHeaders ?? {});
2469 if (headers.has(NONCE_HEADER)) {
2470 return init;
2471 }
2472 headers.set(NONCE_HEADER, nonce);
2473 return { ...init ?? {}, headers };
2474 }
2475 function readRestNonce() {
2476 if (typeof window === "undefined") {
2477 return void 0;
2478 }
2479 const cfg = window.desktopModeConfig;
2480 const value = cfg?.restNonce;
2481 return typeof value === "string" && value.length > 0 ? value : void 0;
2482 }
2483 function resolveUrl(input) {
2484 try {
2485 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
2486 if (typeof input === "string") {
2487 return new URL(input, base);
2488 }
2489 if (input instanceof URL) {
2490 return input;
2491 }
2492 if (typeof Request !== "undefined" && input instanceof Request) {
2493 return new URL(input.url, base);
2494 }
2495 return null;
2496 } catch {
2497 return null;
2498 }
2499 }
2500 function isSameOriginRestUrl(url) {
2501 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
2502 return false;
2503 }
2504 if (url.pathname.includes("/wp-json/")) {
2505 return true;
2506 }
2507 if (url.searchParams.has("rest_route")) {
2508 return true;
2509 }
2510 return false;
2511 }
2512 function trackedFetch(input, init, opts = {}) {
2513 const fn = window.wp?.desktop?.fetch;
2514 if (typeof fn === "function") {
2515 return fn(input, init, opts);
2516 }
2517 const finalInit = injectRestNonce(input, init);
2518 return fetch(input, finalInit);
2519 }
2520 function structuredDefaults() {
2521 return {
2522 ...DEFAULTS,
2523 customGradient: { ...DEFAULTS.customGradient },
2524 customImage: null,
2525 ai: { ...DEFAULTS.ai },
2526 // Clone the collection fields too. A shallow `...DEFAULTS`
2527 // aliases these nested objects, so a later in-place mutation
2528 // (e.g. dragging the gradient editor after a Reset, which spreads
2529 // these defaults into live state) would corrupt the module-level
2530 // DEFAULTS singleton for the rest of the session.
2531 //
2532 // These are one-level clones, which is sufficient *because* all
2533 // three defaults are empty (`{}` / `[]`) — there are no inner
2534 // objects to share. If `DEFAULTS.dockPromotedPositions` ever
2535 // ships seeded entries, its `{ x, y }` values would need a
2536 // deeper clone here.
2537 itemVisibility: { ...DEFAULTS.itemVisibility },
2538 dockOrder: [...DEFAULTS.dockOrder],
2539 dockPromotedPositions: { ...DEFAULTS.dockPromotedPositions }
2540 };
2541 }
2542 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
2543 function resolveSlot() {
2544 const w = window;
2545 let slot = w[SHARED_STORES_SLOT];
2546 if (!slot) {
2547 slot = /* @__PURE__ */ new Map();
2548 w[SHARED_STORES_SLOT] = slot;
2549 }
2550 return slot;
2551 }
2552 function createSharedStore(key, initialState) {
2553 const slot = resolveSlot();
2554 let record = slot.get(key);
2555 if (!record) {
2556 record = {
2557 state: initialState(),
2558 listeners: /* @__PURE__ */ new Set(),
2559 rebuild: initialState
2560 };
2561 slot.set(key, record);
2562 }
2563 const handle = {
2564 // `record.state` is the live reference. The getter on the
2565 // `state` field reads the latest value even if `reset()`
2566 // reassigned it to a fresh object.
2567 get state() {
2568 return record.state;
2569 },
2570 set state(next) {
2571 record.state = next;
2572 },
2573 getState() {
2574 return record.state;
2575 },
2576 notify() {
2577 for (const cb of Array.from(record.listeners)) {
2578 try {
2579 cb(record.state);
2580 } catch (err) {
2581 console.error(
2582 `[desktop-mode/shared-store:${key}] subscriber threw:`,
2583 err
2584 );
2585 }
2586 }
2587 },
2588 subscribe(cb) {
2589 record.listeners.add(cb);
2590 return () => {
2591 record.listeners.delete(cb);
2592 };
2593 },
2594 setState(patch) {
2595 const cur = record.state;
2596 if (typeof cur !== "object" || cur === null) {
2597 console.warn(
2598 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
2599 );
2600 return;
2601 }
2602 Object.assign(cur, patch);
2603 handle.notify();
2604 },
2605 reset() {
2606 const fresh = record.rebuild();
2607 const cur = record.state;
2608 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
2609 const target = cur;
2610 for (const k of Object.keys(target)) {
2611 delete target[k];
2612 }
2613 Object.assign(target, fresh);
2614 } else {
2615 record.state = fresh;
2616 }
2617 record.listeners.clear();
2618 }
2619 };
2620 return handle;
2621 }
2622 const store$3 = createSharedStore(
2623 "desktop-mode/settings-tab-registry",
2624 () => ({
2625 registry: /* @__PURE__ */ new Map(),
2626 listeners: /* @__PURE__ */ new Set()
2627 })
2628 );
2629 const registry$2 = store$3.state.registry;
2630 const listeners$3 = store$3.state.listeners;
2631 function listSettingsTabs() {
2632 return Array.from(registry$2.values()).sort(
2633 (a, b) => (a.order ?? 100) - (b.order ?? 100)
2634 );
2635 }
2636 function subscribeSettingsTabs(cb) {
2637 listeners$3.add(cb);
2638 return () => {
2639 listeners$3.delete(cb);
2640 };
2641 }
2642 let loadPromise = null;
2643 function loadImpl(scriptUrl) {
2644 if (window.desktopModeMountAboutScene) {
2645 return Promise.resolve(window.desktopModeMountAboutScene);
2646 }
2647 if (loadPromise) {
2648 return loadPromise;
2649 }
2650 loadPromise = new Promise((resolve, reject) => {
2651 const existing = document.querySelector(
2652 'script[data-desktop-mode-about-scene="1"]'
2653 );
2654 const finish = () => {
2655 const fn = window.desktopModeMountAboutScene;
2656 if (!fn) {
2657 reject(
2658 new Error(
2659 "[desktop-mode] about-scene bundle loaded but did not register desktopModeMountAboutScene"
2660 )
2661 );
2662 return;
2663 }
2664 resolve(fn);
2665 };
2666 if (existing) {
2667 if (window.desktopModeMountAboutScene) {
2668 finish();
2669 } else {
2670 existing.addEventListener("load", finish);
2671 existing.addEventListener(
2672 "error",
2673 () => reject(new Error("failed to load about-scene bundle"))
2674 );
2675 }
2676 return;
2677 }
2678 const s = document.createElement("script");
2679 s.src = scriptUrl;
2680 s.async = true;
2681 s.dataset.desktopModeAboutScene = "1";
2682 s.addEventListener("load", finish);
2683 s.addEventListener(
2684 "error",
2685 () => reject(new Error("failed to load about-scene bundle"))
2686 );
2687 document.head.appendChild(s);
2688 });
2689 return loadPromise;
2690 }
2691 async function mountAboutSceneLazy(opts, scriptUrl) {
2692 const fn = await loadImpl(scriptUrl);
2693 return fn(opts);
2694 }
2695 function waitForSize(el) {
2696 if (el.clientWidth > 0 && el.clientHeight > 0) {
2697 return Promise.resolve();
2698 }
2699 return new Promise((resolve) => {
2700 const observer = new ResizeObserver(() => {
2701 if (el.clientWidth > 0 && el.clientHeight > 0) {
2702 observer.disconnect();
2703 resolve();
2704 }
2705 });
2706 observer.observe(el);
2707 });
2708 }
2709 function buildAboutSection() {
2710 const wrapper = document.createElement("div");
2711 wrapper.classList.add("desktop-mode-os-settings__about");
2712 const config = window.desktopModeConfig ?? {};
2713 const pluginUrl = config.pluginUrl ?? "";
2714 const version = config.pluginVersion ?? "";
2715 const aboutSceneBundleUrl = config.aboutSceneBundleUrl ?? "";
2716 const desktopApi = window.wp?.desktop;
2717 render(
2718 html`
2719 <div
2720 class="desktop-mode-os-settings__about-stage-host"
2721 data-about-stage
2722 ></div>
2723 `,
2724 wrapper
2725 );
2726 let scene = null;
2727 let aborted = false;
2728 const tearDown = () => {
2729 aborted = true;
2730 if (scene) {
2731 try {
2732 scene.destroy();
2733 } catch {
2734 }
2735 scene = null;
2736 }
2737 };
2738 const mount = async () => {
2739 if (aborted || !wrapper.isConnected) {
2740 return;
2741 }
2742 const host = wrapper.querySelector("[data-about-stage]");
2743 if (!host) {
2744 return;
2745 }
2746 try {
2747 if (desktopApi?.loadModules) {
2748 await desktopApi.loadModules(["pixijs"]);
2749 }
2750 if (aborted || !wrapper.isConnected) {
2751 return;
2752 }
2753 await waitForSize(host);
2754 if (aborted || !wrapper.isConnected) {
2755 return;
2756 }
2757 const built = await mountAboutSceneLazy(
2758 {
2759 container: host,
2760 logoUrl: `${pluginUrl}/assets/images/automattic-logotype-color.png`,
2761 prefersReducedMotion: typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches,
2762 labels: {
2763 eyebrow: __("WordPress Desktop Mode"),
2764 title: __("Crafted with curiosity"),
2765 byline: __("an experiment by Automattic"),
2766 version: version ? `${__("Version")} ${version}` : "",
2767 hint: __("Move your cursor through the swarm · click for a spark")
2768 }
2769 },
2770 aboutSceneBundleUrl
2771 );
2772 if (aborted || !wrapper.isConnected) {
2773 built.destroy();
2774 return;
2775 }
2776 scene = built;
2777 } catch (err) {
2778 if (typeof console !== "undefined") {
2779 console.error("[desktop-mode/about] scene mount failed:", err);
2780 }
2781 }
2782 };
2783 requestAnimationFrame(() => {
2784 void mount();
2785 });
2786 const observer = new MutationObserver(() => {
2787 if (!wrapper.isConnected) {
2788 tearDown();
2789 observer.disconnect();
2790 }
2791 });
2792 observer.observe(document.body, { childList: true, subtree: true });
2793 return wrapper;
2794 }
2795 function translateAccentLabel(id, fallback) {
2796 switch (id) {
2797 case "wp-blue":
2798 return __("WordPress Blue");
2799 case "indigo":
2800 return __("Indigo");
2801 case "teal":
2802 return __("Teal");
2803 case "emerald":
2804 return __("Emerald");
2805 case "amber":
2806 return __("Amber");
2807 case "rose":
2808 return __("Rose");
2809 default:
2810 return fallback;
2811 }
2812 }
2813 function translateDockSizeLabel(id, fallback) {
2814 switch (id) {
2815 case "compact":
2816 return __("Compact");
2817 case "default":
2818 return __("Default");
2819 case "large":
2820 return __("Large");
2821 default:
2822 return fallback;
2823 }
2824 }
2825 function translateDesktopLayoutLabel(id, fallback) {
2826 switch (id) {
2827 case "classic":
2828 return __("Classic");
2829 case "unified":
2830 return __("Unified");
2831 case "spatial":
2832 return __("Spatial");
2833 default:
2834 return fallback;
2835 }
2836 }
2837 function translateDesktopLayoutDescription(id) {
2838 switch (id) {
2839 case "classic":
2840 return __(
2841 "Side bar with the core admin menus, plus a bottom dock for plugin apps."
2842 );
2843 case "unified":
2844 return __(
2845 "Single bottom dock holding every menu — core and plugin apps share one rail."
2846 );
2847 case "spatial":
2848 return __(
2849 "Bottom dock for plugin apps; core admin menus appear as icons on the wallpaper."
2850 );
2851 default:
2852 return "";
2853 }
2854 }
2855 function buildAccentSection(ctx) {
2856 const onPick = (e) => {
2857 const id = e.detail?.value ?? "";
2858 if (!getAccents().some((a) => a.id === id)) {
2859 return;
2860 }
2861 ctx.state.accent = id;
2862 ctx.save();
2863 ctx.apply();
2864 paint();
2865 };
2866 const wrapper = document.createElement("div");
2867 const paint = () => render(
2868 html`
2869 <wpd-section
2870 heading=${__("Accent color")}
2871 description=${__("Used in focused window title bars, buttons, and focus rings.")}
2872 >
2873 <wpd-swatch-grid
2874 label=${__("Accent color")}
2875 mode="row"
2876 @wpd-pick=${onPick}
2877 >
2878 ${getAccents().map(
2879 (a) => html`<wpd-swatch
2880 value=${a.id}
2881 label=${translateAccentLabel(a.id, a.label)}
2882 preview=${a.value}
2883 size="small"
2884 ?selected=${ctx.state.accent === a.id}
2885 ></wpd-swatch>`
2886 )}
2887 </wpd-swatch-grid>
2888 </wpd-section>
2889 `,
2890 wrapper
2891 );
2892 paint();
2893 return wrapper;
2894 }
2895 function buildAiSection(ctx) {
2896 const wrapper = document.createElement("div");
2897 const onToggle = (e) => {
2898 const checked = e.detail?.checked === true;
2899 ctx.state.ai = { ...ctx.state.ai, enabled: checked };
2900 ctx.save();
2901 paint();
2902 };
2903 const onProvider = (e) => {
2904 const id = e.detail?.value ?? "";
2905 if (!getAiProviders().some((p) => p.id === id)) {
2906 return;
2907 }
2908 const prev = ctx.state.ai.provider;
2909 const apiKeys = { ...ctx.state.ai.apiKeys ?? {} };
2910 if (ctx.state.ai.apiKey) {
2911 apiKeys[prev] = ctx.state.ai.apiKey;
2912 }
2913 ctx.state.ai = {
2914 ...ctx.state.ai,
2915 provider: id,
2916 apiKeys,
2917 apiKey: apiKeys[id] ?? ""
2918 };
2919 ctx.save();
2920 paint();
2921 };
2922 const onApiKey = (e) => {
2923 const value = e.detail?.value ?? "";
2924 const apiKeys = { ...ctx.state.ai.apiKeys ?? {} };
2925 apiKeys[ctx.state.ai.provider] = value;
2926 ctx.state.ai = { ...ctx.state.ai, apiKey: value, apiKeys };
2927 ctx.save();
2928 };
2929 const onTransport = (e) => {
2930 const id = e.detail?.value ?? "";
2931 if (!AI_TRANSPORTS.some((t) => t.id === id)) {
2932 return;
2933 }
2934 ctx.state.ai = { ...ctx.state.ai, transport: id };
2935 ctx.save();
2936 };
2937 const paint = () => {
2938 const platformEnabled = ctx.config.aiPlatformSettings?.enabled === true && !!ctx.config.aiPlatformSettings?.apiKey;
2939 const activeProvider = getAiProviders().find((p) => p.id === ctx.state.ai.provider) ?? getAiProviders()[0];
2940 const apiKeyLabel = activeProvider?.apiKeyLabel ?? __("API key");
2941 render(
2942 html`
2943 <wpd-section
2944 heading=${__("AI integration")}
2945 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.")}
2946 >
2947 <wpd-checkbox-label
2948 label=${__("Enable AI features")}
2949 ?checked=${ctx.state.ai.enabled}
2950 @wpd-checkbox-change=${onToggle}
2951 ></wpd-checkbox-label>
2952
2953 <wpd-select
2954 label=${__("Provider")}
2955 value=${ctx.state.ai.provider}
2956 ?disabled=${!ctx.state.ai.enabled}
2957 @wpd-pick=${onProvider}
2958 >
2959 ${getAiProviders().map(
2960 (p) => html`<wpd-option value=${p.id}>${p.label}</wpd-option>`
2961 )}
2962 </wpd-select>
2963
2964 <wpd-text-field
2965 label=${apiKeyLabel}
2966 type="password"
2967 reveal
2968 autocomplete="off"
2969 placeholder=${platformEnabled ? __("Using platform key — enter to override") : __("sk-…")}
2970 value=${ctx.state.ai.apiKey}
2971 ?disabled=${!ctx.state.ai.enabled}
2972 @wpd-input-change=${onApiKey}
2973 ></wpd-text-field>
2974
2975 <wpd-select
2976 label=${__("Live progress updates")}
2977 value=${ctx.state.ai.transport}
2978 ?disabled=${!ctx.state.ai.enabled}
2979 @wpd-pick=${onTransport}
2980 >
2981 ${AI_TRANSPORTS.map(
2982 (t) => html`<wpd-option value=${t.id}>${t.label}</wpd-option>`
2983 )}
2984 </wpd-select>
2985 <p class="desktop-mode-ext__hint">
2986 ${__('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).')}
2987 </p>
2988 </wpd-section>
2989
2990 ${ctx.config.isAdmin ? _buildGlobalSection(ctx) : html``}
2991 `,
2992 wrapper
2993 );
2994 };
2995 paint();
2996 return wrapper;
2997 }
2998 function _buildGlobalSection(ctx) {
2999 const { aiPlatformSettingsUrl: url, restNonce: nonce, aiPlatformSettings: initial } = ctx.config;
3000 const state = {
3001 enabled: initial?.enabled ?? false,
3002 provider: initial?.provider ?? "openai",
3003 apiKey: initial?.apiKey ?? "",
3004 saving: false,
3005 error: ""
3006 };
3007 const el = document.createElement("div");
3008 const save = async () => {
3009 if (!url || !nonce || state.saving) {
3010 return;
3011 }
3012 state.saving = true;
3013 state.error = "";
3014 paint();
3015 try {
3016 const res = await trackedFetch(
3017 url,
3018 {
3019 method: "POST",
3020 headers: {
3021 "Content-Type": "application/json",
3022 "X-WP-Nonce": nonce
3023 },
3024 body: JSON.stringify({
3025 settings: {
3026 enabled: state.enabled,
3027 provider: state.provider,
3028 apiKey: state.apiKey
3029 }
3030 })
3031 },
3032 { source: "desktop-mode/settings/ai" }
3033 );
3034 if (!res.ok) {
3035 const err = await res.json().catch(() => ({}));
3036 state.error = err.message ?? `Error ${res.status}`;
3037 } else {
3038 const saved = await res.json().catch(() => null);
3039 if (saved && typeof saved === "object") {
3040 ctx.config.aiPlatformSettings = saved;
3041 }
3042 }
3043 } catch {
3044 state.error = __("Network error — check your connection.");
3045 } finally {
3046 state.saving = false;
3047 paint();
3048 }
3049 };
3050 const onToggle = (e) => {
3051 state.enabled = e.detail?.checked === true;
3052 save();
3053 };
3054 const onProvider = (e) => {
3055 const id = e.detail?.value ?? "";
3056 if (!getAiProviders().some((p) => p.id === id)) {
3057 return;
3058 }
3059 state.provider = id;
3060 save();
3061 };
3062 const onApiKey = (e) => {
3063 state.apiKey = e.detail?.value ?? "";
3064 };
3065 const onApiKeyCommit = () => {
3066 save();
3067 };
3068 const paint = () => render(
3069 html`
3070 <wpd-section
3071 heading=${__("Global settings")}
3072 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.")}
3073 >
3074 <wpd-checkbox-label
3075 label=${__("Enable AI for all users")}
3076 ?checked=${state.enabled}
3077 @wpd-checkbox-change=${onToggle}
3078 ></wpd-checkbox-label>
3079
3080 <wpd-select
3081 label=${__("Provider")}
3082 value=${state.provider}
3083 ?disabled=${!state.enabled || state.saving}
3084 @wpd-pick=${onProvider}
3085 >
3086 ${getAiProviders().map(
3087 (p) => html`<wpd-option value=${p.id}>${p.label}</wpd-option>`
3088 )}
3089 </wpd-select>
3090
3091 <wpd-text-field
3092 label=${__("Platform API key")}
3093 type="password"
3094 reveal
3095 autocomplete="off"
3096 placeholder=${__("sk-…")}
3097 value=${state.apiKey}
3098 ?disabled=${!state.enabled || state.saving}
3099 @wpd-input-change=${onApiKey}
3100 @wpd-input-commit=${onApiKeyCommit}
3101 @wpd-submit=${onApiKeyCommit}
3102 ></wpd-text-field>
3103
3104 ${state.error ? html`<p class="desktop-mode-ai-settings__error">${state.error}</p>` : html``}
3105 ${state.saving ? html`<p class="desktop-mode-ai-settings__saving">${__("Saving…")}</p>` : html``}
3106 </wpd-section>
3107 `,
3108 el
3109 );
3110 paint();
3111 return el;
3112 }
3113 function hashTitleToHue(input) {
3114 if (!input) {
3115 return 214;
3116 }
3117 let hash = 5381;
3118 for (let i = 0; i < input.length; i++) {
3119 hash = Math.imul(hash, 33) + input.charCodeAt(i);
3120 }
3121 return (hash % 360 + 360) % 360;
3122 }
3123 function renderIcon(icon, opts) {
3124 const className = opts.className ?? "";
3125 const title = opts.title ?? "";
3126 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
3127 const el = document.createElement("span");
3128 el.className = `dashicons ${icon} ${className}`.trim();
3129 el.setAttribute("aria-hidden", "true");
3130 return el;
3131 }
3132 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
3133 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
3134 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
3135 const el = document.createElement("span");
3136 el.className = className;
3137 el.setAttribute("aria-hidden", "true");
3138 el.style.backgroundImage = `url("${icon}")`;
3139 el.style.backgroundRepeat = "no-repeat";
3140 el.style.backgroundPosition = "center";
3141 el.style.backgroundSize = "contain";
3142 el.style.display = "inline-block";
3143 return el;
3144 }
3145 }
3146 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
3147 const commaIdx = icon.indexOf(",");
3148 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
3149 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
3150 return makeImgIcon(icon, className);
3151 }
3152 }
3153 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
3154 return makeImgIcon(icon, className);
3155 }
3156 const span = document.createElement("span");
3157 span.className = `${className} desktop-mode-icon-letter`.trim();
3158 span.setAttribute("aria-hidden", "true");
3159 const letters = letterFromTitle(title);
3160 span.textContent = letters;
3161 const hue = hashTitleToHue(title);
3162 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
3163 span.style.color = "#fff";
3164 span.style.display = "inline-flex";
3165 span.style.alignItems = "center";
3166 span.style.justifyContent = "center";
3167 span.style.fontWeight = "600";
3168 span.style.borderRadius = "4px";
3169 return span;
3170 }
3171 function makeImgIcon(src, className) {
3172 const img = document.createElement("img");
3173 img.className = className;
3174 img.src = src;
3175 img.alt = "";
3176 img.setAttribute("aria-hidden", "true");
3177 img.draggable = false;
3178 return img;
3179 }
3180 function letterFromTitle(title) {
3181 const trimmed = (title ?? "").trim();
3182 if (trimmed === "") {
3183 return "?";
3184 }
3185 const words = trimmed.split(/\s+/);
3186 if (words.length >= 2) {
3187 return (words[0][0] + words[1][0]).toUpperCase();
3188 }
3189 const first = words[0];
3190 if (first.length >= 2) {
3191 return first.slice(0, 2).toUpperCase();
3192 }
3193 return first.toUpperCase();
3194 }
3195 function resolvePlacement(id, nativeRail, visibility) {
3196 const override = visibility[id];
3197 if (override) {
3198 return override;
3199 }
3200 return nativeRail;
3201 }
3202 function listPlaceableItems(dockItems, desktopIcons, visibility) {
3203 const out = [];
3204 const seen = /* @__PURE__ */ new Set();
3205 for (const item of dockItems) {
3206 if (seen.has(item.id)) {
3207 continue;
3208 }
3209 seen.add(item.id);
3210 out.push({
3211 id: item.id,
3212 title: item.title,
3213 icon: item.icon,
3214 nativeRail: "dock",
3215 placement: resolvePlacement(item.id, "dock", visibility)
3216 });
3217 }
3218 for (const icon of desktopIcons) {
3219 if (seen.has(icon.id)) {
3220 continue;
3221 }
3222 seen.add(icon.id);
3223 out.push({
3224 id: icon.id,
3225 title: icon.title,
3226 icon: icon.icon,
3227 nativeRail: "desktop",
3228 placement: resolvePlacement(icon.id, "desktop", visibility)
3229 });
3230 }
3231 out.sort(
3232 (a, b) => a.title.localeCompare(b.title, void 0, { sensitivity: "base" })
3233 );
3234 return out;
3235 }
3236 function readDockItems() {
3237 const api = window.wp?.desktop;
3238 if (api && typeof api.getMenuItems === "function") {
3239 return api.getMenuItems();
3240 }
3241 const cfg = window.desktopModeConfig;
3242 const raw = cfg?.dockItems ?? [];
3243 return raw.map((i) => ({
3244 id: i.id,
3245 title: i.title,
3246 icon: i.icon,
3247 url: i.url,
3248 badge: i.badge,
3249 submenu: i.submenu,
3250 multi: i.multi,
3251 isCore: i.isCore
3252 }));
3253 }
3254 function readDesktopIcons() {
3255 const cfg = window.desktopModeConfig;
3256 return cfg?.desktopIcons ?? [];
3257 }
3258 function getPlacementOptions() {
3259 return [
3260 { id: "desktop", label: __("On the desktop") },
3261 { id: "dock", label: __("On the dock") },
3262 { id: "both", label: __("On both") },
3263 { id: "hidden", label: __("Hidden") }
3264 ];
3265 }
3266 function buildAppsIconsSection(ctx) {
3267 const wrapper = document.createElement("div");
3268 const setPlacement = (id, placement) => {
3269 const next = { ...ctx.state.itemVisibility };
3270 next[id] = placement;
3271 ctx.state.itemVisibility = next;
3272 ctx.save();
3273 paint();
3274 };
3275 const onPlacementChange = (id) => (e) => {
3276 const detail = e.detail;
3277 const next = detail?.value;
3278 if (next === "both" || next === "dock" || next === "desktop" || next === "hidden") {
3279 setPlacement(id, next);
3280 }
3281 };
3282 const paint = (visibility = ctx.state.itemVisibility) => {
3283 const dockItems = readDockItems();
3284 const desktopIcons = readDesktopIcons();
3285 const rows = listPlaceableItems(dockItems, desktopIcons, visibility);
3286 render(
3287 html`
3288 <wpd-section
3289 heading=${__("Apps & Icons")}
3290 description=${__(
3291 "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."
3292 )}
3293 >
3294 ${rows.length === 0 ? html`<wpd-empty-state
3295 heading=${__("No apps registered yet")}
3296 description=${__(
3297 "Plugins and the admin menu will appear here once they’re registered."
3298 )}
3299 ></wpd-empty-state>` : html`<div class="desktop-mode-apps-icons__list">
3300 ${rows.map(
3301 (row) => html`<div
3302 class="desktop-mode-apps-icons__row"
3303 data-item-id=${row.id}
3304 >
3305 <div class="desktop-mode-apps-icons__identity">
3306 ${renderIcon(row.icon, {
3307 title: row.title,
3308 className: "desktop-mode-apps-icons__icon"
3309 })}
3310 <div class="desktop-mode-apps-icons__title">
3311 ${row.title}
3312 </div>
3313 </div>
3314 <wpd-select
3315 label=${__("Show in")}
3316 value=${row.placement}
3317 @wpd-pick=${onPlacementChange(row.id)}
3318 >
3319 ${getPlacementOptions().map(
3320 (o) => html`<wpd-option
3321 value=${o.id}
3322 >${o.label}</wpd-option
3323 >`
3324 )}
3325 </wpd-select>
3326 </div>`
3327 )}
3328 </div>`}
3329 </wpd-section>
3330 `,
3331 wrapper
3332 );
3333 };
3334 paint();
3335 const wpDesktop = window.wp?.desktop;
3336 if (wpDesktop?.subscribeOsSettings) {
3337 const unsubscribe = wpDesktop.subscribeOsSettings((snapshot) => {
3338 if (!wrapper.isConnected) {
3339 unsubscribe();
3340 return;
3341 }
3342 paint(snapshot.itemVisibility);
3343 });
3344 }
3345 return wrapper;
3346 }
3347 function buildDesktopLayoutSection(ctx) {
3348 const onPick = (e) => {
3349 const id = e.detail?.value ?? "";
3350 if (!DESKTOP_LAYOUTS.some((l) => l.id === id)) {
3351 return;
3352 }
3353 ctx.state.desktopLayout = id;
3354 ctx.save();
3355 ctx.apply();
3356 paint();
3357 };
3358 const wrapper = document.createElement("div");
3359 const paint = () => render(
3360 html`
3361 <wpd-section
3362 heading=${__("Desktop layout")}
3363 description=${translateDesktopLayoutDescription(
3364 ctx.state.desktopLayout
3365 )}
3366 >
3367 <wpd-segmented
3368 value=${ctx.state.desktopLayout}
3369 label=${__("Desktop layout")}
3370 @wpd-pick=${onPick}
3371 >
3372 ${DESKTOP_LAYOUTS.map(
3373 (l) => html`<wpd-segment value=${l.id}
3374 >${translateDesktopLayoutLabel(
3375 l.id,
3376 l.label
3377 )}</wpd-segment
3378 >`
3379 )}
3380 </wpd-segmented>
3381 </wpd-section>
3382 `,
3383 wrapper
3384 );
3385 paint();
3386 return wrapper;
3387 }
3388 function buildDockSizeSection(ctx) {
3389 const onPick = (e) => {
3390 const id = e.detail?.value ?? "";
3391 if (!DOCK_SIZES.some((d) => d.id === id)) {
3392 return;
3393 }
3394 ctx.state.dockSize = id;
3395 ctx.save();
3396 ctx.apply();
3397 paint();
3398 };
3399 const wrapper = document.createElement("div");
3400 const paint = () => render(
3401 html`
3402 <wpd-section
3403 heading=${__("Dock size")}
3404 description=${__("Width of the dock and size of its icons.")}
3405 >
3406 <wpd-segmented
3407 value=${ctx.state.dockSize}
3408 label=${__("Dock size")}
3409 @wpd-pick=${onPick}
3410 >
3411 ${DOCK_SIZES.map(
3412 (s) => html`<wpd-segment value=${s.id}
3413 >${translateDockSizeLabel(s.id, s.label)}</wpd-segment
3414 >`
3415 )}
3416 </wpd-segmented>
3417 </wpd-section>
3418 `,
3419 wrapper
3420 );
3421 paint();
3422 return wrapper;
3423 }
3424 const store$2 = createSharedStore(
3425 "desktop-mode/dock-rail-registry",
3426 () => ({
3427 registry: /* @__PURE__ */ new Map(),
3428 listeners: /* @__PURE__ */ new Set(),
3429 activeId: "default"
3430 })
3431 );
3432 const registry$1 = store$2.state.registry;
3433 const listeners$2 = store$2.state.listeners;
3434 function list() {
3435 return Array.from(registry$1.values());
3436 }
3437 function subscribe$1(cb) {
3438 listeners$2.add(cb);
3439 return () => {
3440 listeners$2.delete(cb);
3441 };
3442 }
3443 function getWpHooks() {
3444 const hooks = window.wp?.hooks;
3445 if (!hooks) {
3446 throw new Error(
3447 "[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."
3448 );
3449 }
3450 return hooks;
3451 }
3452 function addAction(hookName2, namespace, callback, priority) {
3453 getWpHooks().addAction(
3454 hookName2,
3455 namespace,
3456 callback,
3457 priority
3458 );
3459 }
3460 function removeAction(hookName2, namespace) {
3461 return getWpHooks().removeAction(hookName2, namespace);
3462 }
3463 function applyFilters(hookName2, value, ...args) {
3464 return getWpHooks().applyFilters(hookName2, value, ...args);
3465 }
3466 function doAction(hookName2, ...args) {
3467 getWpHooks().doAction(hookName2, ...args);
3468 }
3469 const HOOKS = {
3470 /** Filter, receives the wallpaper registry array. */
3471 WALLPAPERS: "desktop-mode.wallpapers",
3472 /** Filter, receives the unfocused-window effect registry array. */
3473 UNFOCUS_EFFECTS: "desktop-mode.unfocus-effects"
3474 };
3475 const HOOK_PREFIX = "desktop-mode.activity.";
3476 function hookName(channel) {
3477 return `${HOOK_PREFIX}${String(channel)}`;
3478 }
3479 let subscribeSeq = 0;
3480 const activity = {
3481 publish(channel, payload) {
3482 doAction(hookName(channel), payload);
3483 },
3484 subscribe(channel, cb) {
3485 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
3486 const hook = hookName(channel);
3487 addAction(
3488 hook,
3489 ns,
3490 (payload) => cb(payload)
3491 );
3492 let removed = false;
3493 return () => {
3494 if (removed) {
3495 return;
3496 }
3497 removed = true;
3498 removeAction(hook, ns);
3499 };
3500 },
3501 filter(channel, value, ...args) {
3502 return applyFilters(hookName(channel), value, ...args);
3503 }
3504 };
3505 const CANARY_TAG = "wpd-confirm-dialog";
3506 let inflight = null;
3507 function isLoaded() {
3508 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
3509 }
3510 function injectScript(scriptUrl) {
3511 return new Promise((resolve, reject) => {
3512 const existing = document.querySelector(
3513 'script[data-desktop-mode-shell-overlays="1"]'
3514 );
3515 const finish = () => {
3516 if (isLoaded()) {
3517 resolve();
3518 return;
3519 }
3520 reject(
3521 new Error(
3522 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
3523 )
3524 );
3525 };
3526 if (existing) {
3527 if (isLoaded()) {
3528 finish();
3529 } else {
3530 existing.addEventListener("load", finish);
3531 existing.addEventListener(
3532 "error",
3533 () => reject(new Error("failed to load shell-overlays bundle"))
3534 );
3535 }
3536 return;
3537 }
3538 const s = document.createElement("script");
3539 s.src = scriptUrl;
3540 s.async = true;
3541 s.dataset.desktopModeShellOverlays = "1";
3542 s.addEventListener("load", finish);
3543 s.addEventListener(
3544 "error",
3545 () => reject(new Error("failed to load shell-overlays bundle"))
3546 );
3547 document.head.appendChild(s);
3548 });
3549 }
3550 function ensureShellOverlaysLoaded(scriptUrl) {
3551 if (isLoaded()) {
3552 return Promise.resolve();
3553 }
3554 if (!scriptUrl) {
3555 return Promise.resolve();
3556 }
3557 if (!inflight) {
3558 inflight = injectScript(scriptUrl);
3559 }
3560 return inflight;
3561 }
3562 function shellOverlaysBundleUrl() {
3563 const cfg = window.desktopModeConfig;
3564 return cfg?.shellOverlaysBundleUrl ?? "";
3565 }
3566 function openWithShellOverlays(isStillCurrent, fn) {
3567 const url = shellOverlaysBundleUrl();
3568 if (isLoaded() || !url) {
3569 fn();
3570 return;
3571 }
3572 void ensureShellOverlaysLoaded(url).then(() => {
3573 if (!isStillCurrent()) {
3574 return;
3575 }
3576 fn();
3577 }).catch((err) => {
3578 if (typeof console !== "undefined") {
3579 console.warn(
3580 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
3581 err
3582 );
3583 }
3584 });
3585 }
3586 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 )}`;
3587 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
3588 constructor() {
3589 super(...arguments);
3590 this._onKey = (e) => {
3591 if (e.key === "Escape") {
3592 e.preventDefault();
3593 this._cancel();
3594 }
3595 if (e.key === "Enter" && !e.isComposing) {
3596 e.preventDefault();
3597 this._confirm();
3598 }
3599 };
3600 this._onBackdrop = (e) => {
3601 const path = e.composedPath();
3602 const original = path.length > 0 ? path[0] : e.target;
3603 if (original === this) {
3604 this._cancel();
3605 }
3606 };
3607 this._confirm = () => {
3608 this.emit("wpd-confirm", { confirmed: true });
3609 this.removeAttribute("open");
3610 };
3611 this._cancel = () => {
3612 this.emit("wpd-cancel", { confirmed: false });
3613 this.removeAttribute("open");
3614 };
3615 }
3616 connectedCallback() {
3617 super.connectedCallback();
3618 this.setAttribute("role", "dialog");
3619 this.setAttribute("aria-modal", "true");
3620 this.addEventListener("keydown", this._onKey);
3621 this.addEventListener("click", this._onBackdrop);
3622 }
3623 disconnectedCallback() {
3624 this.removeEventListener("keydown", this._onKey);
3625 this.removeEventListener("click", this._onBackdrop);
3626 }
3627 render() {
3628 const title = this.title ?? "";
3629 const message = this.message ?? "";
3630 const confirmLabel = this["confirm-label"] || "Confirm";
3631 const cancelLabel = this["cancel-label"] || "Cancel";
3632 const isDanger = this.hasAttribute("danger");
3633 const hideCancel = this.hasAttribute("hide-cancel");
3634 const isDismissable = this.hasAttribute("dismissable");
3635 return html`
3636 <div class="dialog" tabindex="-1">
3637 ${isDismissable ? html`<button
3638 type="button"
3639 class="close"
3640 aria-label="Close"
3641 @click=${() => this._cancel()}
3642 >&times;</button>` : html``}
3643 ${title ? html`<h2 class="title">${title}</h2>` : html``}
3644 ${message ? html`<p class="message">${message}</p>` : html``}
3645 <div class="actions">
3646 ${hideCancel ? html`` : html`<button
3647 type="button"
3648 class="btn btn--secondary"
3649 @click=${() => this._cancel()}
3650 >
3651 ${cancelLabel}
3652 </button>`}
3653 <button
3654 type="button"
3655 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
3656 @click=${() => this._confirm()}
3657 >
3658 ${confirmLabel}
3659 </button>
3660 </div>
3661 </div>
3662 `;
3663 }
3664 };
3665 _WpdConfirmDialog.props = [
3666 "open",
3667 "title",
3668 "message",
3669 "confirm-label",
3670 "cancel-label",
3671 "danger",
3672 "hide-cancel",
3673 "dismissable"
3674 ];
3675 _WpdConfirmDialog.styles = [dialogStyles];
3676 _WpdConfirmDialog.help = {
3677 title: "Confirm dialog",
3678 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.",
3679 status: "experimental",
3680 since: "0.9.0",
3681 props: [
3682 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
3683 { name: "title", type: "string", description: "Heading shown at the top." },
3684 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
3685 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
3686 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
3687 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
3688 { 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." },
3689 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
3690 ],
3691 events: [
3692 {
3693 name: "wpd-confirm",
3694 description: "Fires on confirm. Detail: `{ confirmed: true }`."
3695 },
3696 {
3697 name: "wpd-cancel",
3698 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
3699 }
3700 ]
3701 };
3702 let WpdConfirmDialog = _WpdConfirmDialog;
3703 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
3704 function wpdConfirm(options) {
3705 return new Promise((resolve) => {
3706 const dialog = document.createElement("wpd-confirm-dialog");
3707 dialog.setAttribute("open", "");
3708 if (options.title) {
3709 dialog.setAttribute("title", options.title);
3710 }
3711 dialog.setAttribute("message", options.message);
3712 if (options.confirmLabel) {
3713 dialog.setAttribute("confirm-label", options.confirmLabel);
3714 }
3715 if (options.cancelLabel) {
3716 dialog.setAttribute("cancel-label", options.cancelLabel);
3717 }
3718 {
3719 dialog.setAttribute("danger", "");
3720 }
3721 if (options.hideCancel) {
3722 dialog.setAttribute("hide-cancel", "");
3723 }
3724 if (options.dismissable) {
3725 dialog.setAttribute("dismissable", "");
3726 }
3727 const cleanup = (ok) => {
3728 dialog.remove();
3729 resolve(ok);
3730 };
3731 dialog.addEventListener("wpd-confirm", () => cleanup(true));
3732 dialog.addEventListener("wpd-cancel", () => cleanup(false));
3733 document.body.appendChild(dialog);
3734 const inner = dialog.shadowRoot?.querySelector(".dialog");
3735 (inner ?? dialog).focus?.();
3736 });
3737 }
3738 const DEFAULT_DURATION_MS = 4e3;
3739 const FADE_OUT_MS = 200;
3740 function showToast(options) {
3741 const intent = activity.filter(
3742 "desktop-mode/toast-requested",
3743 { ...options }
3744 );
3745 if (!intent || intent.cancel === true) {
3746 return () => void 0;
3747 }
3748 let dismissRequested = false;
3749 let realDismiss = null;
3750 openWithShellOverlays(
3751 () => !dismissRequested,
3752 () => {
3753 realDismiss = renderToast(intent);
3754 }
3755 );
3756 return () => {
3757 dismissRequested = true;
3758 if (realDismiss) {
3759 realDismiss();
3760 }
3761 };
3762 }
3763 function renderToast(intent) {
3764 const container = ensureContainer();
3765 const toast = document.createElement("wpd-toast");
3766 toast.textContent = intent.message;
3767 if (intent.action) {
3768 toast.setAttribute("action", intent.action.label);
3769 toast.addEventListener("wpd-toast-action", () => {
3770 intent.action?.onClick();
3771 dismiss();
3772 });
3773 }
3774 container.appendChild(toast);
3775 let dismissed = false;
3776 let dismissTimer = null;
3777 const dismiss = () => {
3778 if (dismissed) {
3779 return;
3780 }
3781 dismissed = true;
3782 if (dismissTimer !== null) {
3783 window.clearTimeout(dismissTimer);
3784 dismissTimer = null;
3785 }
3786 toast.setAttribute("state", "out");
3787 window.setTimeout(() => {
3788 toast.remove();
3789 }, FADE_OUT_MS);
3790 };
3791 requestAnimationFrame(() => {
3792 toast.setAttribute("state", "in");
3793 });
3794 dismissTimer = window.setTimeout(
3795 dismiss,
3796 intent.duration ?? DEFAULT_DURATION_MS
3797 );
3798 activity.publish("desktop-mode/toast-shown", { ...intent });
3799 return dismiss;
3800 }
3801 function ensureContainer() {
3802 const existing = document.querySelector(
3803 "wpd-toast-container"
3804 );
3805 if (existing) {
3806 return existing;
3807 }
3808 const el = document.createElement("wpd-toast-container");
3809 document.body.appendChild(el);
3810 return el;
3811 }
3812 createSharedStore(
3813 "desktop-mode/native-url-remap",
3814 () => ({ remaps: [], deps: null })
3815 );
3816 function buildDockRailRendererSection(ctx) {
3817 const wrapper = document.createElement("div");
3818 const onPick = (e) => {
3819 const id = e.detail?.value ?? "";
3820 if (id === "") {
3821 return;
3822 }
3823 ctx.state.dockRailRenderer = id;
3824 ctx.save();
3825 ctx.apply();
3826 paint();
3827 };
3828 let renderers = list();
3829 const paint = () => {
3830 if (renderers.length <= 1) {
3831 render(html``, wrapper);
3832 return;
3833 }
3834 render(
3835 html`
3836 <wpd-section
3837 heading=${__("Dock style")}
3838 description=${__(
3839 "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."
3840 )}
3841 >
3842 <wpd-segmented
3843 value=${ctx.state.dockRailRenderer}
3844 label=${__("Dock style")}
3845 @wpd-pick=${onPick}
3846 >
3847 ${renderers.map(
3848 (r) => html`<wpd-segment value=${r.id}
3849 >${r.label}</wpd-segment
3850 >`
3851 )}
3852 </wpd-segmented>
3853 </wpd-section>
3854 `,
3855 wrapper
3856 );
3857 };
3858 const unsubscribe = subscribe$1(() => {
3859 renderers = list();
3860 paint();
3861 });
3862 const observer = new MutationObserver(() => {
3863 if (!wrapper.isConnected) {
3864 unsubscribe();
3865 observer.disconnect();
3866 }
3867 });
3868 queueMicrotask(() => {
3869 if (wrapper.parentNode) {
3870 observer.observe(wrapper.parentNode, {
3871 childList: true,
3872 subtree: false
3873 });
3874 }
3875 });
3876 paint();
3877 return wrapper;
3878 }
3879 function collectRegistrationErrors(def, checks) {
3880 if (!def || typeof def !== "object") {
3881 return ["def (not an object)"];
3882 }
3883 const d = def;
3884 const errors = [];
3885 for (const check of checks) {
3886 if (!check.valid(d)) {
3887 errors.push(`${check.field} (${check.message})`);
3888 }
3889 }
3890 return errors;
3891 }
3892 class RegistrationError extends Error {
3893 constructor(kind, errors, def) {
3894 super(
3895 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + "."
3896 );
3897 this.name = "RegistrationError";
3898 this.kind = kind;
3899 this.errors = errors;
3900 this.def = def;
3901 }
3902 }
3903 function throwOnRegistrationErrors(kind, errors, def) {
3904 if (errors.length === 0) {
3905 return;
3906 }
3907 throw new RegistrationError(kind, errors, def);
3908 }
3909 const UNFOCUS_EFFECT_NONE = "none";
3910 const store$1 = createSharedStore(
3911 "desktop-mode/unfocus-effect-registry",
3912 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
3913 );
3914 const registry = store$1.state.registry;
3915 const listeners$1 = store$1.state.listeners;
3916 const UNFOCUS_EFFECT_ID = /^[a-z0-9_/-]+$/;
3917 function registerUnfocusEffect(def) {
3918 const errors = [];
3919 if (!def || typeof def !== "object") {
3920 errors.push("def (not an object)");
3921 } else {
3922 if (typeof def.id !== "string" || def.id.trim() === "") {
3923 errors.push("id (missing)");
3924 } else if (!UNFOCUS_EFFECT_ID.test(def.id.trim().toLowerCase())) {
3925 errors.push(
3926 `id (must match ${UNFOCUS_EFFECT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
3927 );
3928 } else if (def.id.trim().toLowerCase() === UNFOCUS_EFFECT_NONE) {
3929 errors.push('id ("none" is reserved)');
3930 }
3931 if (typeof def.label !== "string" || def.label.trim() === "") {
3932 errors.push("label (missing)");
3933 }
3934 if (typeof def.className !== "string" && typeof def.apply !== "function") {
3935 errors.push(
3936 "className|apply (at least one must be provided — a CSS class to toggle or an apply callback)"
3937 );
3938 }
3939 }
3940 throwOnRegistrationErrors("UnfocusEffect", errors, def);
3941 const id = def.id.trim().toLowerCase();
3942 registry.set(id, { ...def, id });
3943 notify$1();
3944 }
3945 function listUnfocusEffects() {
3946 const copy = Array.from(registry.values());
3947 const filtered = applyFilters(
3948 HOOKS.UNFOCUS_EFFECTS,
3949 copy
3950 );
3951 if (!Array.isArray(filtered)) {
3952 if (typeof console !== "undefined") {
3953 console.warn(
3954 "[desktop-mode] `desktop-mode.unfocus-effects` filter returned a non-array; falling back to registry list."
3955 );
3956 }
3957 return copy;
3958 }
3959 return filtered;
3960 }
3961 function subscribeUnfocusEffects(cb) {
3962 listeners$1.add(cb);
3963 return () => {
3964 listeners$1.delete(cb);
3965 };
3966 }
3967 function notify$1() {
3968 const snapshot = Array.from(listeners$1);
3969 for (const cb of snapshot) {
3970 try {
3971 cb();
3972 } catch (err) {
3973 if (typeof console !== "undefined") {
3974 console.error(
3975 "[desktop-mode] unfocus-effect registry listener threw:",
3976 err
3977 );
3978 }
3979 }
3980 }
3981 }
3982 registerUnfocusEffect({
3983 id: "darken",
3984 label: __("Darken"),
3985 description: __("Dim unfocused windows so the focused one stands out."),
3986 className: "desktop-mode-window--fx-darken"
3987 });
3988 registerUnfocusEffect({
3989 id: "frost",
3990 label: __("Frost"),
3991 description: __(
3992 "Throw unfocused windows out of focus — a soft, frosted-glass blur, as if you were looking at them through an iced-over pane."
3993 ),
3994 className: "desktop-mode-window--fx-frost"
3995 });
3996 registerUnfocusEffect({
3997 id: "grayscale",
3998 label: __("Grayscale"),
3999 description: __(
4000 "Drain the colour from unfocused windows so the focused one is the only thing still in colour — your eye snaps right to it."
4001 ),
4002 className: "desktop-mode-window--fx-grayscale"
4003 });
4004 function buildEffectsSection(ctx) {
4005 const wrapper = document.createElement("div");
4006 const onPick = (e) => {
4007 const id = e.detail?.value ?? "";
4008 if (id === "") {
4009 return;
4010 }
4011 if (id !== UNFOCUS_EFFECT_NONE && !effects.some((fx) => fx.id === id)) {
4012 return;
4013 }
4014 ctx.state.unfocusEffect = id;
4015 ctx.save();
4016 ctx.apply();
4017 paint();
4018 };
4019 let effects = listUnfocusEffects();
4020 const paint = () => {
4021 const active = effects.find(
4022 (fx) => fx.id === ctx.state.unfocusEffect
4023 );
4024 const fallbackDescription = __(
4025 "Apply a visual treatment to every window except the one you are working in."
4026 );
4027 const description = ctx.state.unfocusEffect !== UNFOCUS_EFFECT_NONE && active?.description ? active.description : fallbackDescription;
4028 render(
4029 html`
4030 <wpd-section
4031 heading=${__("Unfocused windows")}
4032 description=${description}
4033 >
4034 <wpd-select
4035 value=${ctx.state.unfocusEffect}
4036 label=${__("Unfocused window effect")}
4037 @wpd-pick=${onPick}
4038 >
4039 <wpd-option value=${UNFOCUS_EFFECT_NONE}>
4040 ${__("None")}
4041 </wpd-option>
4042 ${effects.map(
4043 (fx) => html`<wpd-option value=${fx.id}
4044 >${fx.label}</wpd-option
4045 >`
4046 )}
4047 </wpd-select>
4048 </wpd-section>
4049 `,
4050 wrapper
4051 );
4052 };
4053 const unsubscribe = subscribeUnfocusEffects(() => {
4054 effects = listUnfocusEffects();
4055 paint();
4056 });
4057 const observer = new MutationObserver(() => {
4058 if (!wrapper.isConnected) {
4059 unsubscribe();
4060 observer.disconnect();
4061 }
4062 });
4063 queueMicrotask(() => {
4064 if (wrapper.parentNode) {
4065 observer.observe(wrapper.parentNode, {
4066 childList: true,
4067 subtree: false
4068 });
4069 }
4070 });
4071 paint();
4072 return wrapper;
4073 }
4074 function buildExtendedSection(ctx) {
4075 const { extendedOptions, extendedOptionsUrl, restNonce } = ctx.config;
4076 const state = {
4077 media_library_enhanced: extendedOptions?.media_library_enhanced === true,
4078 saving: false,
4079 error: ""
4080 };
4081 const el = document.createElement("div");
4082 const save = async () => {
4083 if (!extendedOptionsUrl || !restNonce || state.saving) {
4084 return;
4085 }
4086 state.saving = true;
4087 state.error = "";
4088 paint();
4089 try {
4090 const res = await trackedFetch(
4091 extendedOptionsUrl,
4092 {
4093 method: "POST",
4094 headers: {
4095 "Content-Type": "application/json",
4096 "X-WP-Nonce": restNonce
4097 },
4098 body: JSON.stringify({
4099 options: {
4100 media_library_enhanced: state.media_library_enhanced
4101 }
4102 })
4103 },
4104 { source: "desktop-mode/settings/extended" }
4105 );
4106 if (!res.ok) {
4107 const err = await res.json().catch(() => ({}));
4108 state.error = err.message ?? `Error ${res.status}`;
4109 } else {
4110 const saved = await res.json().catch(() => null);
4111 if (saved && typeof saved === "object") {
4112 ctx.config.extendedOptions = saved;
4113 }
4114 }
4115 } catch {
4116 state.error = __("Network error — check your connection.");
4117 } finally {
4118 state.saving = false;
4119 paint();
4120 }
4121 };
4122 const onMediaToggle = (e) => {
4123 state.media_library_enhanced = e.detail?.checked === true;
4124 save();
4125 };
4126 const paint = () => render(
4127 html`
4128 <wpd-section
4129 heading=${__("Extended options")}
4130 description=${__(
4131 "Site-wide enhancements that apply to every user. Toggling requires the affected page to be reloaded for the change to take effect."
4132 )}
4133 >
4134 <wpd-checkbox-label
4135 label=${__("Enable drag-and-drop in the Media Library")}
4136 ?checked=${state.media_library_enhanced}
4137 @wpd-checkbox-change=${onMediaToggle}
4138 ></wpd-checkbox-label>
4139
4140 <p class="desktop-mode-ext__hint">
4141 ${__(
4142 "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."
4143 )}
4144 </p>
4145
4146 ${state.error ? html`<p class="desktop-mode-ext__error">${state.error}</p>` : html``}
4147 ${state.saving ? html`<p class="desktop-mode-ext__saving">${__("Saving…")}</p>` : html``}
4148 </wpd-section>
4149 `,
4150 el
4151 );
4152 paint();
4153 return el;
4154 }
4155 function buildFeaturesSection(ctx) {
4156 const wrapper = document.createElement("div");
4157 const onNativePostsToggle = (e) => {
4158 const checked = e.detail?.checked === true;
4159 ctx.state.nativePostsEnabled = checked;
4160 ctx.save();
4161 paint();
4162 };
4163 const onHeartbeatRateChange = (e) => {
4164 const raw = e.detail?.value;
4165 const next = Number(raw);
4166 if (![15, 30, 45, 60].includes(next)) {
4167 return;
4168 }
4169 ctx.state.heartbeatRate = next;
4170 ctx.save();
4171 try {
4172 const wp = window.wp;
4173 const speed = next >= 60 ? "slow" : "standard";
4174 wp?.heartbeat?.interval?.(speed);
4175 } catch (_e) {
4176 }
4177 paint();
4178 };
4179 const onNativePagesToggle = (e) => {
4180 const checked = e.detail?.checked === true;
4181 ctx.state.nativePagesEnabled = checked;
4182 ctx.save();
4183 paint();
4184 };
4185 const onNativeUsersToggle = (e) => {
4186 const checked = e.detail?.checked === true;
4187 ctx.state.nativeUsersEnabled = checked;
4188 ctx.save();
4189 paint();
4190 };
4191 const onNativePluginsToggle = (e) => {
4192 const checked = e.detail?.checked === true;
4193 ctx.state.nativePluginsEnabled = checked;
4194 ctx.save();
4195 paint();
4196 };
4197 const onNativeCommentsToggle = (e) => {
4198 const checked = e.detail?.checked === true;
4199 ctx.state.nativeCommentsEnabled = checked;
4200 ctx.save();
4201 paint();
4202 };
4203 const onShowDesktopOnClickToggle = (e) => {
4204 const checked = e.detail?.checked === true;
4205 ctx.state.showDesktopOnWallpaperClick = checked;
4206 ctx.save();
4207 paint();
4208 };
4209 const onShowPostStatusRibbonsToggle = (e) => {
4210 const checked = e.detail?.checked === true;
4211 ctx.state.showPostStatusRibbons = checked;
4212 ctx.save();
4213 paint();
4214 };
4215 const onFolderSharingToggle = (e) => {
4216 const checked = e.detail?.checked === true;
4217 ctx.state.foldersSharingEnabled = checked;
4218 ctx.save();
4219 paint();
4220 };
4221 let purging = false;
4222 const onPurgeShareTables = async () => {
4223 if (purging) {
4224 return;
4225 }
4226 const ok = await wpdConfirm({
4227 title: __("Delete folder sharing data?"),
4228 message: __(
4229 "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."
4230 ),
4231 confirmLabel: __("Delete data")
4232 });
4233 if (!ok) {
4234 return;
4235 }
4236 const base = shellCfg?.filesUrl;
4237 const nonce = shellCfg?.restNonce;
4238 if (!base || !nonce) {
4239 showToast({ message: __("Files REST endpoint is not available.") });
4240 return;
4241 }
4242 purging = true;
4243 paint();
4244 try {
4245 const url = base.replace(/\/+$/, "") + "/folder-sharing-tables/purge";
4246 const res = await trackedFetch(
4247 url,
4248 {
4249 method: "POST",
4250 headers: { "X-WP-Nonce": nonce },
4251 credentials: "same-origin"
4252 },
4253 { source: "os-settings/folder-sharing-purge" }
4254 );
4255 if (!res.ok) {
4256 const body = await res.text();
4257 throw new Error(`${res.status}: ${body.slice(0, 200)}`);
4258 }
4259 const data = await res.json();
4260 showToast({
4261 message: __("Folder sharing data deleted.") + " (" + data.dropped.length + " tables)"
4262 });
4263 } catch (err) {
4264 const detail = err instanceof Error ? err.message : String(err);
4265 showToast({
4266 message: __("Could not delete sharing data.") + " " + detail
4267 });
4268 } finally {
4269 purging = false;
4270 paint();
4271 }
4272 };
4273 const shellCfg = window.desktopModeConfig;
4274 const aiState = {
4275 enabled: shellCfg?.commentsAi?.enabled ?? false,
4276 providerConfigured: shellCfg?.commentsAi?.providerConfigured ?? false,
4277 saving: false
4278 };
4279 const onCommentsAiToggle = async (e) => {
4280 const checked = e.detail?.checked === true;
4281 if (!shellCfg?.commentsAiUrl || aiState.saving) {
4282 return;
4283 }
4284 aiState.saving = true;
4285 aiState.enabled = checked;
4286 paint();
4287 try {
4288 const response = await trackedFetch(
4289 shellCfg.commentsAiUrl,
4290 {
4291 method: "POST",
4292 credentials: "same-origin",
4293 headers: {
4294 "Content-Type": "application/json",
4295 "X-WP-Nonce": shellCfg.restNonce ?? ""
4296 },
4297 body: JSON.stringify({ enabled: checked })
4298 },
4299 { source: "os-settings/comments-ai" }
4300 );
4301 if (response.ok) {
4302 const json = await response.json();
4303 aiState.enabled = json.enabled;
4304 aiState.providerConfigured = json.providerConfigured;
4305 if (shellCfg.commentsAi) {
4306 shellCfg.commentsAi.enabled = json.enabled;
4307 shellCfg.commentsAi.providerConfigured = json.providerConfigured;
4308 }
4309 } else {
4310 aiState.enabled = !checked;
4311 }
4312 } catch {
4313 aiState.enabled = !checked;
4314 }
4315 aiState.saving = false;
4316 paint();
4317 };
4318 let resetting = false;
4319 const onResetIntros = async () => {
4320 if (resetting) {
4321 return;
4322 }
4323 const cfg = window.desktopModeConfig;
4324 if (!cfg?.seenIntrosUrl) {
4325 return;
4326 }
4327 resetting = true;
4328 paint();
4329 try {
4330 await trackedFetch(
4331 cfg.seenIntrosUrl,
4332 {
4333 method: "DELETE",
4334 credentials: "same-origin",
4335 headers: {
4336 "X-WP-Nonce": cfg.restNonce ?? ""
4337 }
4338 },
4339 { source: "os-settings/reset-intros" }
4340 );
4341 const store2 = window.desktopModeWindowConfig;
4342 if (store2) {
4343 Object.values(store2).forEach((entry) => {
4344 if (entry && typeof entry === "object") {
4345 entry.introSeen = false;
4346 }
4347 });
4348 }
4349 document.dispatchEvent(
4350 new CustomEvent("desktop-mode-intros-reset")
4351 );
4352 } catch {
4353 }
4354 resetting = false;
4355 paint();
4356 };
4357 const paint = () => render(
4358 html`
4359 <wpd-section
4360 heading=${__("Beta features")}
4361 description=${__(
4362 "Experimental redesigns of core admin screens. Off by default — opt in to try them. Each toggle affects only your account and takes effect immediately, no reload required."
4363 )}
4364 >
4365 <div class="desktop-mode-features__item">
4366 <wpd-checkbox-label
4367 label=${__("Use the native Posts window")}
4368 ?checked=${ctx.state.nativePostsEnabled}
4369 @wpd-checkbox-change=${onNativePostsToggle}
4370 ></wpd-checkbox-label>
4371 <p class="desktop-mode-features__hint">
4372 ${__(
4373 "Beta — off by default. Turn on to replace 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. Toggle off any time to return to the classic screen."
4374 )}
4375 </p>
4376 </div>
4377 <div class="desktop-mode-features__item">
4378 <wpd-checkbox-label
4379 label=${__("Use the native Pages window")}
4380 ?checked=${ctx.state.nativePagesEnabled}
4381 @wpd-checkbox-change=${onNativePagesToggle}
4382 ></wpd-checkbox-label>
4383 <p class="desktop-mode-features__hint">
4384 ${__(
4385 "Beta — off by default. Turn on for the same table-driven experience as the Posts window, tailored for Pages: a Parent column, hierarchical sort, and a lock indicator when another user is editing a page. Toggle off any time to return to the classic screen."
4386 )}
4387 </p>
4388 </div>
4389 <div class="desktop-mode-features__item">
4390 <wpd-checkbox-label
4391 label=${__("Use the native Users window")}
4392 ?checked=${ctx.state.nativeUsersEnabled}
4393 @wpd-checkbox-change=${onNativeUsersToggle}
4394 ></wpd-checkbox-label>
4395 <p class="desktop-mode-features__hint">
4396 ${__(
4397 "Beta — off by default. Turn on for 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."
4398 )}
4399 </p>
4400 </div>
4401 <div class="desktop-mode-features__item">
4402 <wpd-checkbox-label
4403 label=${__("Use the native Plugins window")}
4404 ?checked=${ctx.state.nativePluginsEnabled}
4405 @wpd-checkbox-change=${onNativePluginsToggle}
4406 ></wpd-checkbox-label>
4407 <p class="desktop-mode-features__hint">
4408 ${__(
4409 "Beta — off by default. Turn on for 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."
4410 )}
4411 </p>
4412 </div>
4413 <div class="desktop-mode-features__item">
4414 <wpd-checkbox-label
4415 label=${__("Use the native Comments window")}
4416 ?checked=${ctx.state.nativeCommentsEnabled}
4417 @wpd-checkbox-change=${onNativeCommentsToggle}
4418 ></wpd-checkbox-label>
4419 <p class="desktop-mode-features__hint">
4420 ${__(
4421 "Beta — off by default. Turn on for 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)."
4422 )}
4423 </p>
4424 </div>
4425 </wpd-section>
4426 <wpd-section
4427 heading=${__("Features")}
4428 description=${__(
4429 "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."
4430 )}
4431 >
4432 ${shellCfg?.commentsAi ? html`
4433 <div class="desktop-mode-features__item">
4434 <wpd-checkbox-label
4435 label=${__("Score new comments with AI")}
4436 ?checked=${aiState.enabled}
4437 ?disabled=${aiState.saving || !aiState.providerConfigured}
4438 @wpd-checkbox-change=${onCommentsAiToggle}
4439 ></wpd-checkbox-label>
4440 <p class="desktop-mode-features__hint">
4441 ${aiState.providerConfigured ? __(
4442 "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."
4443 ) : __(
4444 "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."
4445 )}
4446 </p>
4447 </div>
4448 ` : ""}
4449 <div class="desktop-mode-features__item">
4450 <wpd-checkbox-label
4451 label=${__(
4452 "Show desktop when clicking the wallpaper"
4453 )}
4454 ?checked=${ctx.state.showDesktopOnWallpaperClick}
4455 @wpd-checkbox-change=${onShowDesktopOnClickToggle}
4456 ></wpd-checkbox-label>
4457 <p class="desktop-mode-features__hint">
4458 ${__(
4459 '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.'
4460 )}
4461 </p>
4462 </div>
4463 <div class="desktop-mode-features__item">
4464 <wpd-checkbox-label
4465 label=${__(
4466 "Show post/page status ribbon"
4467 )}
4468 ?checked=${ctx.state.showPostStatusRibbons}
4469 @wpd-checkbox-change=${onShowPostStatusRibbonsToggle}
4470 ></wpd-checkbox-label>
4471 <p class="desktop-mode-features__hint">
4472 ${__(
4473 "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."
4474 )}
4475 </p>
4476 </div>
4477 <div class="desktop-mode-features__item">
4478 <wpd-checkbox-label
4479 label=${__("Folder sharing")}
4480 ?checked=${ctx.state.foldersSharingEnabled}
4481 @wpd-checkbox-change=${onFolderSharingToggle}
4482 ></wpd-checkbox-label>
4483 <p class="desktop-mode-features__hint">
4484 ${__(
4485 '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.'
4486 )}
4487 </p>
4488 ${shellCfg?.currentUserIsAdmin ? html`
4489 <div class="desktop-mode-features__danger-row">
4490 <wpd-button
4491 variant="danger"
4492 ?disabled=${purging}
4493 @click=${onPurgeShareTables}
4494 >
4495 ${purging ? __("Deleting…") : __("Delete folder sharing data")}
4496 </wpd-button>
4497 <p class="desktop-mode-features__hint">
4498 ${__(
4499 "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."
4500 )}
4501 </p>
4502 </div>
4503 ` : ""}
4504 </div>
4505 <div class="desktop-mode-features__item">
4506 <label class="desktop-mode-features__select-label">
4507 <span class="desktop-mode-features__select-title">${__(
4508 "WordPress Heartbeat rate"
4509 )}</span>
4510 <wpd-select
4511 value=${String(ctx.state.heartbeatRate)}
4512 @wpd-pick=${onHeartbeatRateChange}
4513 >
4514 <wpd-option value="15">${__("Fast — 15s (not recommended)")}</wpd-option>
4515 <wpd-option value="30">${__("Medium — 30s")}</wpd-option>
4516 <wpd-option value="45">${__("Slow — 45s")}</wpd-option>
4517 <wpd-option value="60">${__("Very slow — 60s (default)")}</wpd-option>
4518 </wpd-select>
4519 </label>
4520 <p class="desktop-mode-features__hint">
4521 ${__(
4522 "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."
4523 )}
4524 </p>
4525 </div>
4526 <div class="desktop-mode-features__row">
4527 <wpd-button
4528 variant="secondary"
4529 ?disabled=${resetting}
4530 @click=${onResetIntros}
4531 >
4532 ${resetting ? __("Resetting…") : __("Reset what’s-new dialogs")}
4533 </wpd-button>
4534 <p class="desktop-mode-features__hint">
4535 ${__(
4536 "Re-shows the one-time introduction dialog the next time you open each redesigned native window."
4537 )}
4538 </p>
4539 </div>
4540 </wpd-section>
4541 `,
4542 wrapper
4543 );
4544 paint();
4545 return wrapper;
4546 }
4547 const WPD_COMPONENT_TAGS = [
4548 "wpd-section",
4549 "wpd-button",
4550 "wpd-swatch",
4551 "wpd-swatch-grid",
4552 "wpd-segmented",
4553 "wpd-segment",
4554 "wpd-select",
4555 "wpd-option",
4556 "wpd-multiselect",
4557 "wpd-color-field",
4558 "wpd-range-field",
4559 "wpd-text-field",
4560 "wpd-number-field",
4561 "wpd-checkbox",
4562 "wpd-checkbox-label",
4563 "wpd-toast",
4564 "wpd-toast-container",
4565 "wpd-tabs",
4566 "wpd-tab",
4567 "wpd-tabpanel",
4568 "wpd-window-button",
4569 "wpd-menu",
4570 "wpd-menu-item",
4571 "wpd-context-menu",
4572 "wpd-context-menu-option",
4573 "wpd-confirm-dialog",
4574 "wpd-modal",
4575 "wpd-user-search",
4576 "wpd-role-picker",
4577 "wpd-flyout",
4578 "wpd-tab-chip",
4579 "wpd-stack",
4580 "wpd-cluster",
4581 "wpd-icon",
4582 "wpd-body",
4583 "wpd-panel",
4584 "wpd-row",
4585 "wpd-grid",
4586 "wpd-display",
4587 "wpd-empty-state",
4588 "wpd-key",
4589 "wpd-code",
4590 "wpd-badge",
4591 "wpd-ribbon",
4592 "wpd-tile",
4593 "wpd-log",
4594 "wpd-steps",
4595 "wpd-step",
4596 "wpd-table",
4597 "wpd-spinner",
4598 "wpd-relative-time",
4599 "wpd-avatar",
4600 "wpd-textarea",
4601 "wpd-chip",
4602 "wpd-tag-input",
4603 "wpd-form",
4604 "wpd-save-status",
4605 "wpd-category-picker",
4606 "wpd-crumb-chain",
4607 "wpd-card",
4608 "wpd-rating-summary",
4609 "wpd-notice",
4610 "wpd-progress-bar"
4611 ];
4612 let demoBannerLogged = false;
4613 function logDemoBanner() {
4614 if (demoBannerLogged) {
4615 return;
4616 }
4617 demoBannerLogged = true;
4618 const headingStyle = [
4619 "background: #ffb400",
4620 "color: #1a1a1a",
4621 "font-weight: 700",
4622 "font-size: 12px",
4623 "padding: 4px 8px",
4624 "border-radius: 3px"
4625 ].join(";");
4626 const bodyStyle = [
4627 "color: #b25c00",
4628 "font-weight: 500"
4629 ].join(";");
4630 console.log(
4631 '%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.',
4632 headingStyle,
4633 bodyStyle
4634 );
4635 }
4636 function buildHelpSection() {
4637 const entries = collectEntries();
4638 const el = document.createElement("div");
4639 el.classList.add("desktop-mode-os-settings__help");
4640 logDemoBanner();
4641 let activeTag = entries[0]?.tag ?? "";
4642 const paint = () => {
4643 const active = entries.find((e) => e.tag === activeTag) ?? entries[0];
4644 render(
4645 html`
4646 <wpd-section
4647 heading=${__("Component library")}
4648 description=${__(
4649 "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."
4650 )}
4651 >
4652 <p class="desktop-mode-os-settings__help-count">
4653 ${String(entries.length)} ${__("components registered.")}
4654 </p>
4655 </wpd-section>
4656
4657 <wpd-section
4658 heading=${__("Missing-import warner — live demo")}
4659 description=${__(
4660 '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.'
4661 )}
4662 >
4663 <div
4664 class="desktop-mode-os-settings__help-warner-demo"
4665 aria-hidden="true"
4666 style="position:absolute;width:0;height:0;overflow:hidden;clip:rect(0 0 0 0);"
4667 >
4668 <!--
4669 Case 1 — invented name, nothing close in the registry.
4670 Triggers the "no component by that name exists" branch.
4671 -->
4672 <wpd-example-console-fail-due-to-unregistered-component></wpd-example-console-fail-due-to-unregistered-component>
4673
4674 <!--
4675 Case 2 — typo within Levenshtein distance of a real tag.
4676 Triggers the "Did you mean <wpd-button>?" branch.
4677 -->
4678 <wpd-buton></wpd-buton>
4679
4680 <!--
4681 Case 3 — looks plausible but is not in the registry.
4682 Triggers the unknown-tag branch with no suggestion.
4683 -->
4684 <wpd-totally-made-up-thing></wpd-totally-made-up-thing>
4685 </div>
4686 </wpd-section>
4687
4688 <div class="desktop-mode-os-settings__help-layout">
4689 <nav
4690 class="desktop-mode-os-settings__help-nav"
4691 aria-label=${__("Components")}
4692 >
4693 ${entries.map(
4694 (entry) => html`
4695 <button
4696 type="button"
4697 class=${classNames(
4698 "desktop-mode-os-settings__help-nav-item",
4699 entry.tag === (active?.tag ?? "") ? "is-active" : ""
4700 )}
4701 aria-pressed=${entry.tag === (active?.tag ?? "") ? "true" : "false"}
4702 @click=${() => {
4703 activeTag = entry.tag;
4704 paint();
4705 }}
4706 >
4707 <span class="desktop-mode-os-settings__help-nav-title"
4708 >${entry.title}</span
4709 >
4710 <span class="desktop-mode-os-settings__help-nav-tag"
4711 >&lt;${entry.tag}&gt;</span
4712 >
4713 </button>
4714 `
4715 )}
4716 </nav>
4717 <div class="desktop-mode-os-settings__help-detail">
4718 ${active ? renderDetail(active) : renderEmpty()}
4719 </div>
4720 </div>
4721 `,
4722 el
4723 );
4724 };
4725 paint();
4726 return el;
4727 }
4728 function renderDetail(entry) {
4729 const help = entry.help;
4730 const status = help?.status ?? "stable";
4731 const since = help?.since;
4732 return html`
4733 <header class="desktop-mode-os-settings__help-head">
4734 <h3 class="desktop-mode-os-settings__help-title">${entry.title}</h3>
4735 <code class="desktop-mode-os-settings__help-code"
4736 >&lt;${entry.tag}&gt;</code
4737 >
4738 <span
4739 class=${classNames(
4740 "desktop-mode-os-settings__help-badge",
4741 `is-${status}`
4742 )}
4743 >${statusLabel(status)}</span
4744 >
4745 ${since ? html`<span class="desktop-mode-os-settings__help-since"
4746 >${__("Since")} ${since}</span
4747 >` : html``}
4748 </header>
4749
4750 ${help?.summary ? html`<p class="desktop-mode-os-settings__help-summary">
4751 ${help.summary}
4752 </p>` : html``}
4753
4754 ${help?.example ? html`
4755 <section class="desktop-mode-os-settings__help-group">
4756 <h4>${__("Example")}</h4>
4757 <div class="desktop-mode-os-settings__help-example">
4758 ${help.example}
4759 </div>
4760 </section>
4761 ` : html``}
4762 ${renderPropsTable(entry, help)} ${renderSlots(help)}
4763 ${renderEvents(help)} ${renderParts(help)}
4764 ${renderCssProps(help)}
4765 ${!help ? html`<p class="desktop-mode-os-settings__help-note">
4766 ${__(
4767 "This component has no help descriptor yet. Add `static help` to its class for a fuller reference."
4768 )}
4769 </p>` : html``}
4770 `;
4771 }
4772 function renderPropsTable(entry, help) {
4773 const documented = help?.props ?? [];
4774 const documentedNames = new Set(documented.map((p) => p.name));
4775 const undocumented = entry.props.filter((p) => !documentedNames.has(p));
4776 if (documented.length === 0 && undocumented.length === 0) {
4777 return html``;
4778 }
4779 return html`
4780 <section class="desktop-mode-os-settings__help-group">
4781 <h4>${__("Props")}</h4>
4782 <table class="desktop-mode-os-settings__help-table">
4783 <thead>
4784 <tr>
4785 <th>${__("Name")}</th>
4786 <th>${__("Type")}</th>
4787 <th>${__("Default")}</th>
4788 <th>${__("Description")}</th>
4789 </tr>
4790 </thead>
4791 <tbody>
4792 ${documented.map(
4793 (p) => html`
4794 <tr>
4795 <td><code>${p.name}</code></td>
4796 <td>${p.type ?? "—"}</td>
4797 <td>${p.default ?? "—"}</td>
4798 <td>${p.description ?? ""}</td>
4799 </tr>
4800 `
4801 )}
4802 ${undocumented.map(
4803 (name) => html`
4804 <tr>
4805 <td><code>${name}</code></td>
4806 <td>—</td>
4807 <td>—</td>
4808 <td>
4809 <em
4810 >${__(
4811 "Undocumented — declared via static props."
4812 )}</em
4813 >
4814 </td>
4815 </tr>
4816 `
4817 )}
4818 </tbody>
4819 </table>
4820 </section>
4821 `;
4822 }
4823 function renderSlots(help) {
4824 if (!help?.slots?.length) {
4825 return html``;
4826 }
4827 return html`
4828 <section class="desktop-mode-os-settings__help-group">
4829 <h4>${__("Slots")}</h4>
4830 <ul class="desktop-mode-os-settings__help-list">
4831 ${help.slots.map(
4832 (s) => html`
4833 <li>
4834 <code>${s.name}</code>
4835 ${s.description ? html` — ${s.description}` : html``}
4836 </li>
4837 `
4838 )}
4839 </ul>
4840 </section>
4841 `;
4842 }
4843 function renderEvents(help) {
4844 if (!help?.events?.length) {
4845 return html``;
4846 }
4847 return html`
4848 <section class="desktop-mode-os-settings__help-group">
4849 <h4>${__("Events")}</h4>
4850 <ul class="desktop-mode-os-settings__help-list">
4851 ${help.events.map(
4852 (e) => html`
4853 <li>
4854 <code>${e.name}</code>
4855 ${e.detail ? html` — <code>${e.detail}</code>` : html``}
4856 ${e.description ? html` — ${e.description}` : html``}
4857 </li>
4858 `
4859 )}
4860 </ul>
4861 </section>
4862 `;
4863 }
4864 function renderParts(help) {
4865 if (!help?.parts?.length) {
4866 return html``;
4867 }
4868 return html`
4869 <section class="desktop-mode-os-settings__help-group">
4870 <h4>${__("Shadow parts")}</h4>
4871 <ul class="desktop-mode-os-settings__help-list">
4872 ${help.parts.map(
4873 (p) => html`
4874 <li>
4875 <code>::part(${p.name})</code>
4876 ${p.description ? html` — ${p.description}` : html``}
4877 </li>
4878 `
4879 )}
4880 </ul>
4881 </section>
4882 `;
4883 }
4884 function renderCssProps(help) {
4885 if (!help?.cssProps?.length) {
4886 return html``;
4887 }
4888 return html`
4889 <section class="desktop-mode-os-settings__help-group">
4890 <h4>${__("CSS custom properties")}</h4>
4891 <ul class="desktop-mode-os-settings__help-list">
4892 ${help.cssProps.map(
4893 (v) => html`
4894 <li>
4895 <code>${v.name}</code>
4896 ${v.default ? html`
4897 (${__("default")}
4898 <code>${v.default}</code>)
4899 ` : html``}
4900 ${v.description ? html` — ${v.description}` : html``}
4901 </li>
4902 `
4903 )}
4904 </ul>
4905 </section>
4906 `;
4907 }
4908 function renderEmpty() {
4909 return html`<p>${__("No components registered.")}</p>`;
4910 }
4911 function collectEntries() {
4912 const entries = [];
4913 for (const tag of WPD_COMPONENT_TAGS) {
4914 const ctor = customElements.get(tag);
4915 if (!ctor) {
4916 continue;
4917 }
4918 const help = ctor.help ?? null;
4919 const title = help?.title ?? defaultTitleFromTag(tag);
4920 const props = ctor.props ?? [];
4921 entries.push({ tag, title, help, props });
4922 }
4923 entries.sort((a, b) => a.title.localeCompare(b.title));
4924 return entries;
4925 }
4926 function defaultTitleFromTag(tag) {
4927 const bare = tag.replace(/^wpd-/, "").replace(/-/g, " ");
4928 return bare.charAt(0).toUpperCase() + bare.slice(1);
4929 }
4930 function statusLabel(status) {
4931 switch (status) {
4932 case "experimental":
4933 return __("Experimental");
4934 case "planned":
4935 return __("Planned");
4936 case "stable":
4937 default:
4938 return __("Stable");
4939 }
4940 }
4941 function classNames(...parts) {
4942 return parts.filter(Boolean).join(" ");
4943 }
4944 const store = createSharedStore(
4945 "desktop-mode/wallpaper-registry",
4946 () => ({
4947 seed: [],
4948 listeners: /* @__PURE__ */ new Set()
4949 })
4950 );
4951 const seed = store.state.seed;
4952 const listeners = store.state.listeners;
4953 function register(def) {
4954 throwOnRegistrationErrors(
4955 "Wallpaper",
4956 collectRegistrationErrors(def, WALLPAPER_CHECKS),
4957 def
4958 );
4959 const idx = seed.findIndex((w) => w.id === def.id);
4960 if (idx >= 0) {
4961 seed[idx] = def;
4962 } else {
4963 seed.push(def);
4964 }
4965 notify();
4966 }
4967 function unregister(id) {
4968 const idx = seed.findIndex((w) => w.id === id);
4969 if (idx >= 0) {
4970 seed.splice(idx, 1);
4971 notify();
4972 }
4973 }
4974 function subscribe(cb) {
4975 listeners.add(cb);
4976 return () => {
4977 listeners.delete(cb);
4978 };
4979 }
4980 function notify() {
4981 const snapshot = Array.from(listeners);
4982 for (const cb of snapshot) {
4983 try {
4984 cb();
4985 } catch (err) {
4986 if (typeof console !== "undefined") {
4987 console.error(
4988 "[desktop-mode] wallpaper registry listener threw:",
4989 err
4990 );
4991 }
4992 }
4993 }
4994 }
4995 function all() {
4996 const copy = seed.slice();
4997 const filtered = applyFilters(HOOKS.WALLPAPERS, copy);
4998 if (!Array.isArray(filtered)) {
4999 if (typeof console !== "undefined") {
5000 console.warn(
5001 "[desktop-mode] `desktop-mode.wallpapers` filter returned a non-array; falling back to seed list."
5002 );
5003 }
5004 return copy;
5005 }
5006 return filtered.filter(isValidDef);
5007 }
5008 function get(id) {
5009 return all().find((w) => w.id === id);
5010 }
5011 const WALLPAPER_CHECKS = [
5012 {
5013 field: "id",
5014 message: "missing or not a non-empty string",
5015 valid: (d) => typeof d.id === "string" && d.id !== ""
5016 },
5017 {
5018 field: "label",
5019 message: "missing or not a non-empty string",
5020 valid: (d) => typeof d.label === "string" && d.label !== ""
5021 },
5022 {
5023 field: "preview",
5024 message: "missing or not a non-empty string",
5025 valid: (d) => typeof d.preview === "string" && d.preview !== ""
5026 },
5027 {
5028 field: "type",
5029 message: 'must be "css" or "canvas"',
5030 valid: (d) => d.type === "css" || d.type === "canvas"
5031 },
5032 {
5033 field: "value/resolveValue/mount",
5034 message: "css types need `value` or `resolveValue`; canvas types need `mount`",
5035 valid: (d) => {
5036 if (d.type === "css") {
5037 return typeof d.value === "string" || typeof d.resolveValue === "function";
5038 }
5039 if (d.type === "canvas") {
5040 return typeof d.mount === "function";
5041 }
5042 return true;
5043 }
5044 }
5045 ];
5046 function isValidDef(def) {
5047 return collectRegistrationErrors(def, WALLPAPER_CHECKS).length === 0;
5048 }
5049 async function fetchMediaPage(config, page, search, hdOnly) {
5050 const url = new URL(config.mediaUrl);
5051 url.searchParams.set("media_type", "image");
5052 url.searchParams.set("per_page", String(MEDIA_PER_PAGE));
5053 url.searchParams.set("page", String(page));
5054 url.searchParams.set("orderby", "date");
5055 url.searchParams.set("order", "desc");
5056 url.searchParams.set(
5057 "_fields",
5058 "id,source_url,alt_text,title,media_details"
5059 );
5060 if (search) {
5061 url.searchParams.set("search", search);
5062 }
5063 if (hdOnly) {
5064 url.searchParams.set("desktop_mode_min_width", String(HD_MIN_WIDTH));
5065 url.searchParams.set("desktop_mode_min_height", String(HD_MIN_HEIGHT));
5066 }
5067 const response = await trackedFetch(
5068 url.toString(),
5069 {
5070 credentials: "same-origin",
5071 headers: { "X-WP-Nonce": config.restNonce }
5072 },
5073 { source: "desktop-mode/settings/media" }
5074 );
5075 if (!response.ok) {
5076 let message = `HTTP ${response.status}`;
5077 try {
5078 const data = await response.json();
5079 if (data && typeof data.message === "string") {
5080 message = data.message;
5081 }
5082 } catch {
5083 }
5084 throw new Error(message);
5085 }
5086 const totalPagesHeader = response.headers.get("X-WP-TotalPages");
5087 const totalPages = totalPagesHeader ? parseInt(totalPagesHeader, 10) : 1;
5088 const items = await response.json();
5089 return { items: items.filter(isUsableImage), totalPages: totalPages || 1 };
5090 }
5091 async function uploadImage(config, file) {
5092 const response = await trackedFetch(
5093 config.mediaUrl,
5094 {
5095 method: "POST",
5096 credentials: "same-origin",
5097 headers: {
5098 "X-WP-Nonce": config.restNonce,
5099 "Content-Type": file.type,
5100 "Content-Disposition": `attachment; filename="${sanitizeFilename(file.name)}"`
5101 },
5102 body: file
5103 },
5104 { source: "desktop-mode/settings/media-upload" }
5105 );
5106 if (!response.ok) {
5107 let message = `Upload failed (HTTP ${response.status}).`;
5108 try {
5109 const data2 = await response.json();
5110 if (data2 && typeof data2.message === "string") {
5111 message = data2.message;
5112 }
5113 } catch {
5114 }
5115 throw new Error(message);
5116 }
5117 const data = await response.json();
5118 return { id: data.id, url: data.source_url };
5119 }
5120 function buildCustomImageSection(ctx, body) {
5121 const tabDefs = [];
5122 const pane = document.createElement("div");
5123 pane.className = "desktop-mode-os-settings__tab-pane";
5124 if (ctx.config.canUpload) {
5125 tabDefs.push({
5126 key: "upload",
5127 label: __("Upload new"),
5128 render: () => renderUploadPane(ctx, pane, body)
5129 });
5130 }
5131 tabDefs.push({
5132 key: "library",
5133 label: __("Media Library"),
5134 render: () => renderLibraryPane(ctx, pane, body)
5135 });
5136 const initialKey = tabDefs[0].key;
5137 const onTabChange = (e) => {
5138 const key = e.detail.value;
5139 tabDefs.find((t) => t.key === key)?.render();
5140 };
5141 const wrap = document.createElement("div");
5142 render(
5143 html`
5144 <div class="desktop-mode-os-settings__uploader">
5145 <h4 class="desktop-mode-os-settings__uploader-heading">
5146 ${__("Or use your own image")}
5147 </h4>
5148 ${tabDefs.length > 1 ? html`<wpd-tabs
5149 value=${initialKey}
5150 label=${__("Image source")}
5151 @wpd-tab-change=${onTabChange}
5152 >
5153 ${tabDefs.map(
5154 (def) => html`<wpd-tab value=${def.key}
5155 >${def.label}</wpd-tab
5156 >`
5157 )}
5158 </wpd-tabs>` : null}
5159 ${pane}
5160 </div>
5161 `,
5162 wrap
5163 );
5164 tabDefs.find((t) => t.key === initialKey)?.render();
5165 return wrap.firstElementChild;
5166 }
5167 function renderUploadPane(ctx, pane, body) {
5168 const tile = document.createElement("div");
5169 tile.className = "desktop-mode-os-settings__upload-tile";
5170 tile.dataset.wallpaperId = CUSTOM_IMAGE_ID;
5171 tile.setAttribute(
5172 "aria-pressed",
5173 ctx.state.wallpaper === CUSTOM_IMAGE_ID ? "true" : "false"
5174 );
5175 const fileInput = document.createElement("input");
5176 fileInput.type = "file";
5177 fileInput.accept = "image/*";
5178 fileInput.className = "desktop-mode-os-settings__file-input";
5179 fileInput.addEventListener("change", () => {
5180 const file = fileInput.files?.[0];
5181 if (file) {
5182 void handleImageFile(ctx, file, tile, body);
5183 }
5184 fileInput.value = "";
5185 });
5186 render(html`${fileInput}${tile}`, pane);
5187 renderUploadTile(ctx, tile, fileInput, body);
5188 }
5189 function renderUploadTile(ctx, tile, fileInput, body) {
5190 tile.classList.remove("desktop-mode-os-settings__upload-tile--filled");
5191 tile.classList.remove("desktop-mode-os-settings__upload-tile--dragover");
5192 tile.classList.remove("desktop-mode-os-settings__upload-tile--busy");
5193 tile.removeAttribute("aria-label");
5194 const hasImage = !!ctx.state.customImage;
5195 if (hasImage) {
5196 tile.classList.add("desktop-mode-os-settings__upload-tile--filled");
5197 tile.setAttribute("aria-label", __("Custom image wallpaper"));
5198 tile.style.backgroundImage = `url("${encodeURI(ctx.state.customImage.url)}")`;
5199 } else {
5200 tile.style.backgroundImage = "";
5201 tile.setAttribute("aria-label", __("Upload a wallpaper image"));
5202 }
5203 const onRemove = (e) => {
5204 e.stopPropagation();
5205 ctx.state.customImage = null;
5206 if (ctx.state.wallpaper === CUSTOM_IMAGE_ID) {
5207 ctx.state.wallpaper = DEFAULT_WALLPAPER_ID;
5208 }
5209 registerCustomImageIfPresent(ctx.state);
5210 ctx.save();
5211 ctx.apply();
5212 renderUploadTile(ctx, tile, fileInput, body);
5213 refreshWallpaperPressedState(ctx, body);
5214 };
5215 render(
5216 hasImage ? html`
5217 <wpd-button
5218 variant="danger"
5219 class="desktop-mode-os-settings__upload-remove"
5220 aria-label=${__("Remove custom image")}
5221 @click=${onRemove}
5222 >${__("Remove")}</wpd-button
5223 >
5224 ` : html`
5225 <div class="desktop-mode-os-settings__upload-inner">
5226 <span
5227 class="desktop-mode-os-settings__upload-plus"
5228 aria-hidden="true"
5229 >+</span
5230 >
5231 <span class="desktop-mode-os-settings__upload-prompt"
5232 >${__("Drop an image here, or click to upload")}</span
5233 >
5234 <span class="desktop-mode-os-settings__upload-hint"
5235 >${__(
5236 "JPEG, PNG, or WebP · goes straight to your Media Library"
5237 )}</span
5238 >
5239 </div>
5240 `,
5241 tile
5242 );
5243 tile.onclick = () => {
5244 if (tile.classList.contains("desktop-mode-os-settings__upload-tile--busy")) {
5245 return;
5246 }
5247 if (ctx.state.customImage) {
5248 selectWallpaper(ctx, CUSTOM_IMAGE_ID, body);
5249 return;
5250 }
5251 fileInput.click();
5252 };
5253 tile.ondragover = (e) => {
5254 e.preventDefault();
5255 tile.classList.add("desktop-mode-os-settings__upload-tile--dragover");
5256 };
5257 tile.ondragleave = () => {
5258 tile.classList.remove("desktop-mode-os-settings__upload-tile--dragover");
5259 };
5260 tile.ondrop = (e) => {
5261 e.preventDefault();
5262 tile.classList.remove("desktop-mode-os-settings__upload-tile--dragover");
5263 const file = e.dataTransfer?.files?.[0];
5264 if (file) {
5265 void handleImageFile(ctx, file, tile, body);
5266 }
5267 };
5268 }
5269 async function handleImageFile(ctx, file, tile, body) {
5270 if (!file.type.startsWith("image/")) {
5271 showUploadError(tile, __("That file isn’t an image."));
5272 return;
5273 }
5274 tile.classList.add("desktop-mode-os-settings__upload-tile--busy");
5275 render(
5276 html`<span class="desktop-mode-os-settings__upload-status"
5277 >${__("Uploading…")}</span
5278 >`,
5279 tile
5280 );
5281 const fileInput = tile.parentElement?.querySelector(
5282 ".desktop-mode-os-settings__file-input"
5283 );
5284 try {
5285 const media = await uploadImage(ctx.config, file);
5286 ctx.state.customImage = { id: media.id, url: media.url };
5287 ctx.state.wallpaper = CUSTOM_IMAGE_ID;
5288 registerCustomImageIfPresent(ctx.state);
5289 ctx.save();
5290 ctx.apply();
5291 if (fileInput) {
5292 renderUploadTile(ctx, tile, fileInput, body);
5293 }
5294 refreshWallpaperPressedState(ctx, body);
5295 } catch (err) {
5296 tile.classList.remove("desktop-mode-os-settings__upload-tile--busy");
5297 if (fileInput) {
5298 renderUploadTile(ctx, tile, fileInput, body);
5299 }
5300 const message = err instanceof Error ? err.message : __("Upload failed.");
5301 showUploadError(tile, message);
5302 }
5303 }
5304 function showUploadError(tile, message) {
5305 let err = tile.querySelector(".desktop-mode-os-settings__upload-error");
5306 if (!err) {
5307 err = document.createElement("span");
5308 err.className = "desktop-mode-os-settings__upload-error";
5309 err.setAttribute("role", "status");
5310 tile.appendChild(err);
5311 }
5312 err.textContent = message;
5313 window.setTimeout(() => {
5314 err?.remove();
5315 }, 4e3);
5316 }
5317 function renderLibraryPane(ctx, pane, body) {
5318 const search = document.createElement("input");
5319 search.type = "search";
5320 search.placeholder = __("Search your media");
5321 search.className = "desktop-mode-os-settings__library-search";
5322 search.setAttribute("aria-label", __("Search media"));
5323 const grid = document.createElement("div");
5324 grid.className = "desktop-mode-os-settings__library-grid";
5325 const meta = document.createElement("span");
5326 meta.className = "desktop-mode-os-settings__library-meta";
5327 const loadMore = document.createElement("wpd-button");
5328 loadMore.setAttribute("variant", "ghost");
5329 loadMore.textContent = __("Load more");
5330 let query = "";
5331 let page = 0;
5332 let totalPages = 0;
5333 let loaded = [];
5334 let hiddenByHd = 0;
5335 let loading = false;
5336 const onHdToggle = (e) => {
5337 ctx.state.libraryHdOnly = e.detail.checked;
5338 ctx.save();
5339 resetAndReload();
5340 };
5341 render(
5342 html`
5343 <div class="desktop-mode-os-settings__library">
5344 <div class="desktop-mode-os-settings__library-toolbar">
5345 ${search}
5346 <wpd-checkbox-label
5347 label=${sprintf(
5348 // translators: %1$d is the HD minimum width in px, %2$d is the minimum height.
5349 __("Only HD (≥%1$d×%2$d)"),
5350 HD_MIN_WIDTH,
5351 HD_MIN_HEIGHT
5352 )}
5353 ?checked=${ctx.state.libraryHdOnly}
5354 @wpd-checkbox-change=${onHdToggle}
5355 ></wpd-checkbox-label>
5356 </div>
5357 ${grid}
5358 <div class="desktop-mode-os-settings__library-footer">
5359 ${meta}${loadMore}
5360 </div>
5361 </div>
5362 `,
5363 pane
5364 );
5365 const updateMeta = () => {
5366 const visible = visibleLibraryItems(ctx.state, loaded).length;
5367 const parts = [
5368 // translators: %d is the number of media items currently visible.
5369 sprintf(__("Showing %d"), visible)
5370 ];
5371 if (ctx.state.libraryHdOnly && hiddenByHd > 0) {
5372 parts.push(
5373 // translators: %d is the number of images filtered out by the HD toggle.
5374 sprintf(__("%d hidden by HD filter"), hiddenByHd)
5375 );
5376 }
5377 meta.textContent = parts.join(" · ");
5378 loadMore.hidden = page >= totalPages;
5379 if (loading) {
5380 loadMore.setAttribute("disabled", "");
5381 } else {
5382 loadMore.removeAttribute("disabled");
5383 }
5384 };
5385 const renderGrid = () => {
5386 const visible = visibleLibraryItems(ctx.state, loaded);
5387 hiddenByHd = loaded.length - visible.length;
5388 if (visible.length === 0 && !loading) {
5389 render(
5390 html`<p class="desktop-mode-os-settings__library-empty">
5391 ${ctx.state.libraryHdOnly ? __(
5392 "No HD images found. Try unchecking the filter, or upload a larger image."
5393 ) : __("No images in your Media Library yet.")}
5394 </p>`,
5395 grid
5396 );
5397 } else {
5398 grid.innerHTML = "";
5399 for (const item of visible) {
5400 grid.appendChild(buildLibraryTile(ctx, item, body));
5401 }
5402 }
5403 updateMeta();
5404 };
5405 const loadNextPage = async () => {
5406 if (loading || totalPages > 0 && page >= totalPages) {
5407 return;
5408 }
5409 loading = true;
5410 updateMeta();
5411 if (page === 0) {
5412 render(
5413 html`${Array.from(
5414 { length: 8 },
5415 () => html`<div
5416 class="desktop-mode-os-settings__library-tile desktop-mode-os-settings__library-tile--skeleton"
5417 ></div>`
5418 )}`,
5419 grid
5420 );
5421 }
5422 try {
5423 const result = await fetchMediaPage(
5424 ctx.config,
5425 page + 1,
5426 query,
5427 ctx.state.libraryHdOnly
5428 );
5429 page = page + 1;
5430 totalPages = result.totalPages;
5431 loaded = loaded.concat(result.items);
5432 renderGrid();
5433 } catch (err) {
5434 render(
5435 html`<p class="desktop-mode-os-settings__library-error">
5436 ${err instanceof Error ? sprintf(
5437 // translators: %s is the browser-supplied error message.
5438 __("Couldn’t load your media: %s"),
5439 err.message
5440 ) : __("Couldn’t load your media.")}
5441 </p>`,
5442 grid
5443 );
5444 } finally {
5445 loading = false;
5446 updateMeta();
5447 }
5448 };
5449 const resetAndReload = () => {
5450 page = 0;
5451 totalPages = 0;
5452 loaded = [];
5453 hiddenByHd = 0;
5454 void loadNextPage();
5455 };
5456 let searchTimer = null;
5457 search.addEventListener("input", () => {
5458 if (searchTimer !== null) {
5459 window.clearTimeout(searchTimer);
5460 }
5461 searchTimer = window.setTimeout(() => {
5462 searchTimer = null;
5463 query = search.value.trim();
5464 resetAndReload();
5465 }, SEARCH_DEBOUNCE_MS);
5466 });
5467 loadMore.addEventListener("click", () => {
5468 void loadNextPage();
5469 });
5470 void loadNextPage();
5471 }
5472 function visibleLibraryItems(state, items) {
5473 if (!state.libraryHdOnly) {
5474 return items;
5475 }
5476 return items.filter(
5477 (it) => it.media_details.width >= HD_MIN_WIDTH && it.media_details.height >= HD_MIN_HEIGHT
5478 );
5479 }
5480 function buildLibraryTile(ctx, item, body) {
5481 const isSelected = ctx.state.wallpaper === CUSTOM_IMAGE_ID && ctx.state.customImage?.id === item.id;
5482 const sizes = item.media_details.sizes || {};
5483 const thumbUrl = sizes.medium?.source_url || sizes.thumbnail?.source_url || sizes.large?.source_url || item.source_url;
5484 const altOrTitle = item.alt_text || stripHtml(item.title?.rendered || "") || `Image #${item.id}`;
5485 const onClick = () => {
5486 ctx.state.customImage = { id: item.id, url: item.source_url };
5487 ctx.state.wallpaper = CUSTOM_IMAGE_ID;
5488 registerCustomImageIfPresent(ctx.state);
5489 ctx.save();
5490 ctx.apply();
5491 refreshWallpaperPressedState(ctx, body);
5492 const tileGrid = wrapper.firstElementChild?.parentElement;
5493 if (tileGrid) {
5494 tileGrid.querySelectorAll("[data-media-id]").forEach((el) => {
5495 const selected = el.dataset.mediaId === String(item.id);
5496 el.setAttribute("aria-pressed", selected ? "true" : "false");
5497 el.classList.toggle(
5498 "desktop-mode-os-settings__library-tile--selected",
5499 selected
5500 );
5501 });
5502 }
5503 };
5504 const wrapper = document.createElement("div");
5505 render(
5506 html`
5507 <button
5508 type="button"
5509 class=${isSelected ? "desktop-mode-os-settings__library-tile desktop-mode-os-settings__library-tile--selected" : "desktop-mode-os-settings__library-tile"}
5510 data-media-id=${String(item.id)}
5511 aria-pressed=${isSelected ? "true" : "false"}
5512 aria-label=${altOrTitle}
5513 title=${altOrTitle}
5514 style=${`background-image: url("${encodeURI(thumbUrl)}")`}
5515 @click=${onClick}
5516 >
5517 <span class="desktop-mode-os-settings__library-tile-dims"
5518 >${item.media_details.width}×${item.media_details.height}</span
5519 >
5520 </button>
5521 `,
5522 wrapper
5523 );
5524 return wrapper.firstElementChild;
5525 }
5526 function customGradientCss(state) {
5527 const { from, to, angle } = state.customGradient;
5528 return `linear-gradient(${angle}deg, ${from}, ${to})`;
5529 }
5530 function attachCustomGradientEditor(ctx) {
5531 register({
5532 id: CUSTOM_GRADIENT_ID,
5533 label: __("Custom gradient"),
5534 type: "css",
5535 preview: customGradientCss(ctx.state),
5536 resolveValue: () => customGradientCss(ctx.state),
5537 renderEditor: (container) => renderCustomGradientEditor(ctx, container)
5538 });
5539 }
5540 function registerCustomImageIfPresent(state) {
5541 if (!state.customImage) {
5542 unregister(CUSTOM_IMAGE_ID);
5543 return;
5544 }
5545 const safeUrl = encodeURI(state.customImage.url);
5546 const value = `url("${safeUrl}") center/cover no-repeat, #1d2327`;
5547 register({
5548 id: CUSTOM_IMAGE_ID,
5549 label: __("Custom image"),
5550 type: "css",
5551 value,
5552 preview: value
5553 });
5554 }
5555 function selectWallpaper(ctx, id, body) {
5556 ctx.state.wallpaper = id;
5557 ctx.save();
5558 ctx.apply();
5559 refreshWallpaperPressedState(ctx, body);
5560 }
5561 function refreshWallpaperPressedState(ctx, body) {
5562 body.querySelectorAll("[data-wallpaper-id]").forEach((el) => {
5563 const selected = el.dataset.wallpaperId === ctx.state.wallpaper;
5564 if (selected) {
5565 el.setAttribute("selected", "");
5566 } else {
5567 el.removeAttribute("selected");
5568 }
5569 el.setAttribute("aria-pressed", selected ? "true" : "false");
5570 });
5571 }
5572 function syncEditorSlot(ctx, slot, inner, def) {
5573 teardownEditor(ctx);
5574 inner.innerHTML = "";
5575 if (!def.renderEditor) {
5576 slot.dataset.expanded = "false";
5577 return;
5578 }
5579 const editorCtx = {
5580 id: def.id,
5581 pluginUrl: "",
5582 prefersReducedMotion: typeof window.matchMedia === "function" && window.matchMedia("( prefers-reduced-motion: reduce )").matches,
5583 visible: !document.hidden
5584 };
5585 try {
5586 const result = def.renderEditor(inner, editorCtx);
5587 if (isPromise(result)) {
5588 result.then((teardown) => {
5589 ctx.activeEditorTeardown = teardown;
5590 });
5591 } else {
5592 ctx.activeEditorTeardown = result;
5593 }
5594 } catch (err) {
5595 if (typeof console !== "undefined") {
5596 console.error(
5597 `[desktop-mode] Wallpaper "${def.id}" renderEditor threw:`,
5598 err
5599 );
5600 }
5601 }
5602 slot.dataset.expanded = "true";
5603 }
5604 function teardownEditor(ctx) {
5605 if (ctx.activeEditorTeardown) {
5606 try {
5607 ctx.activeEditorTeardown();
5608 } catch (err) {
5609 if (typeof console !== "undefined") {
5610 console.error(
5611 "[desktop-mode] Wallpaper editor teardown threw:",
5612 err
5613 );
5614 }
5615 }
5616 ctx.activeEditorTeardown = null;
5617 }
5618 }
5619 function renderCustomGradientEditor(ctx, container) {
5620 container.classList.add("desktop-mode-os-settings__gradient-editor-inner");
5621 const onFrom = (e) => {
5622 ctx.state.customGradient.from = e.detail.value;
5623 onChange();
5624 };
5625 const onTo = (e) => {
5626 ctx.state.customGradient.to = e.detail.value;
5627 onChange();
5628 };
5629 const onAngle = (e) => {
5630 ctx.state.customGradient.angle = e.detail.value;
5631 onChange();
5632 };
5633 const onChange = () => {
5634 ctx.save();
5635 ctx.apply();
5636 syncGradientPreviewSwatch(ctx, container);
5637 paint();
5638 };
5639 const paint = () => render(
5640 html`
5641 <div class="desktop-mode-os-settings__gradient-row">
5642 <wpd-color-field
5643 variant="block"
5644 label=${__("From")}
5645 value=${ctx.state.customGradient.from}
5646 @wpd-color-change=${onFrom}
5647 ></wpd-color-field>
5648 <wpd-color-field
5649 variant="block"
5650 label=${__("To")}
5651 value=${ctx.state.customGradient.to}
5652 @wpd-color-change=${onTo}
5653 ></wpd-color-field>
5654 </div>
5655 <wpd-range-field
5656 label=${__("Angle")}
5657 min="0"
5658 max="360"
5659 step="1"
5660 suffix="°"
5661 value=${String(ctx.state.customGradient.angle)}
5662 @wpd-range-change=${onAngle}
5663 ></wpd-range-field>
5664 `,
5665 container
5666 );
5667 paint();
5668 return () => {
5669 };
5670 }
5671 function syncGradientPreviewSwatch(ctx, editorEl) {
5672 const section = editorEl.closest("wpd-section");
5673 const preview = section?.querySelector(
5674 `[data-wallpaper-id="${CUSTOM_GRADIENT_ID}"]`
5675 );
5676 if (preview) {
5677 preview.style.background = customGradientCss(ctx.state);
5678 }
5679 }
5680 function buildWallpaperSection(ctx, body) {
5681 const editorSlot = document.createElement("div");
5682 editorSlot.className = "desktop-mode-os-settings__editor-slot";
5683 editorSlot.dataset.expanded = "false";
5684 const editorInner = document.createElement("div");
5685 editorInner.className = "desktop-mode-os-settings__editor-slot-inner";
5686 editorSlot.appendChild(editorInner);
5687 const onPick = (e) => {
5688 const id = e.detail?.value ?? "";
5689 const def = get(id);
5690 if (!def || def.id === CUSTOM_IMAGE_ID) {
5691 return;
5692 }
5693 selectWallpaper(ctx, def.id, body);
5694 syncEditorSlot(ctx, editorSlot, editorInner, def);
5695 paint();
5696 };
5697 const customImageSection = buildCustomImageSection(ctx, body);
5698 const wrapper = document.createElement("div");
5699 const paint = () => render(
5700 html`
5701 <wpd-section
5702 heading=${__("Wallpaper")}
5703 description=${__(
5704 "The backdrop behind your windows. Pick a preset, mix your own gradient, or drop in an image."
5705 )}
5706 >
5707 <div
5708 class="desktop-mode-os-settings__grid desktop-mode-os-settings__grid--wallpapers"
5709 @wpd-pick=${onPick}
5710 >
5711 ${all().filter((def) => def.id !== CUSTOM_IMAGE_ID).map(
5712 (def) => html`<wpd-swatch
5713 value=${def.id}
5714 label=${def.label}
5715 preview=${def.preview}
5716 variant="wallpaper"
5717 data-wallpaper-id=${def.id}
5718 ?selected=${ctx.state.wallpaper === def.id}
5719 >
5720 <span class="desktop-mode-os-settings__swatch-label"
5721 >${def.label}</span
5722 >
5723 </wpd-swatch>`
5724 )}
5725 </div>
5726 ${editorSlot} ${customImageSection}
5727 </wpd-section>
5728 `,
5729 wrapper
5730 );
5731 paint();
5732 const active = get(ctx.state.wallpaper);
5733 if (active) {
5734 syncEditorSlot(ctx, editorSlot, editorInner, active);
5735 }
5736 const unsubscribe = subscribe(() => {
5737 if (!wrapper.isConnected) {
5738 unsubscribe();
5739 return;
5740 }
5741 paint();
5742 const now = get(ctx.state.wallpaper);
5743 if (now) {
5744 syncEditorSlot(ctx, editorSlot, editorInner, now);
5745 }
5746 });
5747 return wrapper;
5748 }
5749 function isTabVisible(tab, isAdmin) {
5750 if (tab.capability && tab.capability === "manage_options") {
5751 return isAdmin;
5752 }
5753 return true;
5754 }
5755 function renderOsSettingsPanel(ctx, body) {
5756 attachCustomGradientEditor(ctx);
5757 teardownEditor(ctx);
5758 if (ctx.tabRegistryUnsubscribe) {
5759 ctx.tabRegistryUnsubscribe();
5760 ctx.tabRegistryUnsubscribe = null;
5761 }
5762 body.classList.add("desktop-mode-os-settings");
5763 const onReset = () => {
5764 const preservedImage = ctx.state.customImage;
5765 ctx.state = { ...structuredDefaults(), customImage: preservedImage };
5766 ctx.save();
5767 ctx.apply();
5768 ctx.renderPanel(body);
5769 };
5770 const isAdmin = ctx.config.isAdmin;
5771 const externalTabs = listSettingsTabs().filter(
5772 (tab) => isTabVisible(tab, isAdmin)
5773 );
5774 const rows = [
5775 {
5776 id: "appearance",
5777 order: 10,
5778 tab: html`<wpd-tab value="appearance"
5779 >${__("Appearance")}</wpd-tab
5780 >`,
5781 panel: html`<wpd-tabpanel for="appearance">
5782 <wpd-panel>
5783 <p class="desktop-mode-os-settings__intro">
5784 ${__(
5785 "Personalize your desktop. Changes apply instantly and are saved to this browser."
5786 )}
5787 </p>
5788 ${buildWallpaperSection(ctx, body)}
5789 ${buildAccentSection(ctx)}
5790 ${buildDesktopLayoutSection(ctx)}
5791 ${buildDockSizeSection(ctx)}
5792 ${buildDockRailRendererSection(ctx)}
5793 </wpd-panel>
5794 </wpd-tabpanel>`
5795 },
5796 {
5797 id: "ai",
5798 order: 20,
5799 tab: html`<wpd-tab value="ai">${__("AI Settings")}</wpd-tab>`,
5800 panel: html`<wpd-tabpanel for="ai">
5801 <wpd-panel>${buildAiSection(ctx)}</wpd-panel>
5802 </wpd-tabpanel>`
5803 },
5804 {
5805 id: "features",
5806 order: 25,
5807 tab: html`<wpd-tab value="features"
5808 >${__("Features")}</wpd-tab
5809 >`,
5810 panel: html`<wpd-tabpanel for="features">
5811 <wpd-panel>${buildFeaturesSection(ctx)}</wpd-panel>
5812 </wpd-tabpanel>`
5813 },
5814 {
5815 id: "apps-icons",
5816 order: 22,
5817 tab: html`<wpd-tab value="apps-icons"
5818 >${__("Apps & Icons")}</wpd-tab
5819 >`,
5820 panel: html`<wpd-tabpanel for="apps-icons">
5821 <wpd-panel>${buildAppsIconsSection(ctx)}</wpd-panel>
5822 </wpd-tabpanel>`
5823 },
5824 {
5825 id: "effects",
5826 order: 27,
5827 tab: html`<wpd-tab value="effects"
5828 >${__("Effects")}</wpd-tab
5829 >`,
5830 panel: html`<wpd-tabpanel for="effects">
5831 <wpd-panel>${buildEffectsSection(ctx)}</wpd-panel>
5832 </wpd-tabpanel>`
5833 }
5834 ];
5835 if (isAdmin) {
5836 rows.push({
5837 id: "extended",
5838 order: 30,
5839 tab: html`<wpd-tab value="extended"
5840 >${__("Extended Options")}</wpd-tab
5841 >`,
5842 panel: html`<wpd-tabpanel for="extended">
5843 <wpd-panel>${buildExtendedSection(ctx)}</wpd-panel>
5844 </wpd-tabpanel>`
5845 });
5846 rows.push({
5847 id: "help",
5848 order: 40,
5849 tab: html`<wpd-tab value="help">${__("Components")}</wpd-tab>`,
5850 panel: html`<wpd-tabpanel for="help">
5851 <wpd-panel>${buildHelpSection()}</wpd-panel>
5852 </wpd-tabpanel>`
5853 });
5854 }
5855 rows.push({
5856 id: "about",
5857 order: Number.MAX_SAFE_INTEGER,
5858 tab: html`<wpd-tab value="about">${__("About")}</wpd-tab>`,
5859 panel: html`<wpd-tabpanel for="about">
5860 <wpd-panel padding="0">${buildAboutSection()}</wpd-panel>
5861 </wpd-tabpanel>`
5862 });
5863 for (const tab of externalTabs) {
5864 const tabId = `ext-${tab.id}`;
5865 const hostAttr = `wpd-settings-tab-host-${tab.id}`;
5866 const tabRef = tab;
5867 rows.push({
5868 id: tabId,
5869 order: tab.order ?? 100,
5870 tab: html`<wpd-tab value=${tabId}>${tab.label}</wpd-tab>`,
5871 panel: html`<wpd-tabpanel for=${tabId}>
5872 <wpd-panel><div data-host=${hostAttr}></div></wpd-panel>
5873 </wpd-tabpanel>`,
5874 mount: (rootBody) => {
5875 const host = rootBody.querySelector(
5876 `[data-host="${hostAttr}"]`
5877 );
5878 if (!host) {
5879 return;
5880 }
5881 try {
5882 tabRef.render(host, {
5883 isAdmin,
5884 getOsSettings: () => ctx.getOsSettingsSnapshot(),
5885 subscribeOsSettings: (cb) => ctx.subscribeOsSettings(cb)
5886 });
5887 } catch (err) {
5888 if (typeof console !== "undefined") {
5889 console.error(
5890 "[desktop-mode] settings tab render threw:",
5891 tabRef.id,
5892 err
5893 );
5894 }
5895 }
5896 }
5897 });
5898 }
5899 rows.sort((a, b) => a.order - b.order);
5900 const previousTabs = body.querySelector("wpd-tabs");
5901 const previousValue = ctx.activeTabId ?? previousTabs?.value ?? previousTabs?.getAttribute("value") ?? "appearance";
5902 const activeRowExists = rows.some((r) => r.id === previousValue);
5903 const initialTab = activeRowExists ? previousValue : "appearance";
5904 render(
5905 html`
5906 <wpd-tabs value=${initialTab} label=${__("Settings sections")}>
5907 ${rows.map((r) => r.tab)}
5908 </wpd-tabs>
5909 ${rows.map((r) => r.panel)}
5910 <wpd-panel class="desktop-mode-os-settings__footer">
5911 <wpd-button variant="ghost" @click=${onReset}
5912 >${__("Reset to defaults")}</wpd-button
5913 >
5914 </wpd-panel>
5915 `,
5916 body
5917 );
5918 for (const row of rows) {
5919 if (row.mount) {
5920 row.mount(body);
5921 }
5922 }
5923 const tabsHost = body.querySelector("wpd-tabs");
5924 if (tabsHost) {
5925 tabsHost.addEventListener("wpd-tab-change", (e) => {
5926 const detail = e.detail;
5927 if (detail?.value) {
5928 ctx.activeTabId = detail.value;
5929 }
5930 });
5931 }
5932 ctx.activeTabId = initialTab;
5933 ctx.tabRegistryUnsubscribe = subscribeSettingsTabs(() => {
5934 if (!body.isConnected) {
5935 if (ctx.tabRegistryUnsubscribe) {
5936 ctx.tabRegistryUnsubscribe();
5937 ctx.tabRegistryUnsubscribe = null;
5938 }
5939 return;
5940 }
5941 ctx.renderPanel(body);
5942 });
5943 }
5944 window.desktopModeRenderOsSettingsPanel = renderOsSettingsPanel;
5945 })();
5946