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

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