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

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

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