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

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

2,583 lines 102.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function() {
2 "use strict";
3 function html(strings, ...values) {
4 return { __wpdHtml: true, strings, values };
5 }
6 function isTemplateResult(v) {
7 return !!v && v.__wpdHtml === true;
8 }
9 const MARKER_PREFIX = "$$wpd$$";
10 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
11 function joinWithMarkers(strings) {
12 let out = strings[0];
13 for (let i = 1; i < strings.length; i++) {
14 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
15 }
16 return out;
17 }
18 const compiledCache = /* @__PURE__ */ new WeakMap();
19 function compile(strings) {
20 const cached = compiledCache.get(strings);
21 if (cached) {
22 return cached;
23 }
24 const template = document.createElement("template");
25 template.innerHTML = joinWithMarkers(strings);
26 const recipes = [];
27 const walk = (node, path) => {
28 if (node.nodeType === Node.ELEMENT_NODE) {
29 const el = node;
30 for (const attr of Array.from(el.attributes)) {
31 const rawName = attr.name;
32 const rawValue = attr.value;
33 const prefix = rawName[0];
34 if (MARKER_RE.test(rawValue)) {
35 MARKER_RE.lastIndex = 0;
36 if (prefix === "@") {
37 const match = MARKER_RE.exec(rawValue);
38 MARKER_RE.lastIndex = 0;
39 recipes.push({
40 path,
41 kind: "event",
42 name: rawName.slice(1),
43 valueIndex: match ? Number(match[1]) : 0
44 });
45 el.removeAttribute(rawName);
46 } else if (prefix === ".") {
47 const match = MARKER_RE.exec(rawValue);
48 MARKER_RE.lastIndex = 0;
49 recipes.push({
50 path,
51 kind: "prop",
52 name: rawName.slice(1),
53 valueIndex: match ? Number(match[1]) : 0
54 });
55 el.removeAttribute(rawName);
56 } else if (prefix === "?") {
57 const match = MARKER_RE.exec(rawValue);
58 MARKER_RE.lastIndex = 0;
59 recipes.push({
60 path,
61 kind: "bool",
62 name: rawName.slice(1),
63 valueIndex: match ? Number(match[1]) : 0
64 });
65 el.removeAttribute(rawName);
66 } else {
67 const fragments = [];
68 const indices = [];
69 let lastEnd = 0;
70 let m;
71 MARKER_RE.lastIndex = 0;
72 while ((m = MARKER_RE.exec(rawValue)) !== null) {
73 fragments.push(rawValue.slice(lastEnd, m.index));
74 indices.push(Number(m[1]));
75 lastEnd = m.index + m[0].length;
76 }
77 fragments.push(rawValue.slice(lastEnd));
78 recipes.push({
79 path,
80 kind: "attr",
81 name: rawName,
82 template: fragments,
83 valueIndices: indices
84 });
85 el.setAttribute(rawName, "");
86 }
87 }
88 }
89 }
90 const children = Array.from(node.childNodes);
91 let shift = 0;
92 for (let i = 0; i < children.length; i++) {
93 const child = children[i];
94 const liveIndex = i + shift;
95 if (child.nodeType === Node.TEXT_NODE) {
96 const text = child.textContent || "";
97 if (!MARKER_RE.test(text)) {
98 MARKER_RE.lastIndex = 0;
99 continue;
100 }
101 MARKER_RE.lastIndex = 0;
102 const parent = child.parentNode;
103 let lastEnd = 0;
104 let m;
105 const newNodes = [];
106 const newRecipes = [];
107 MARKER_RE.lastIndex = 0;
108 while ((m = MARKER_RE.exec(text)) !== null) {
109 if (m.index > lastEnd) {
110 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
111 }
112 const placeholder = document.createTextNode("");
113 newNodes.push(placeholder);
114 newRecipes.push({
115 path: [...path, liveIndex + newNodes.length - 1],
116 kind: "node",
117 valueIndex: Number(m[1])
118 });
119 lastEnd = m.index + m[0].length;
120 }
121 if (lastEnd < text.length) {
122 newNodes.push(document.createTextNode(text.slice(lastEnd)));
123 }
124 for (const nn of newNodes) {
125 parent.insertBefore(nn, child);
126 }
127 parent.removeChild(child);
128 shift += newNodes.length - 1;
129 recipes.push(...newRecipes);
130 } else {
131 walk(child, [...path, liveIndex]);
132 }
133 }
134 };
135 walk(template.content, []);
136 const buildParts = (fragment) => {
137 const out = [];
138 for (const r of recipes) {
139 let node = fragment;
140 for (const idx of r.path) {
141 node = node.childNodes[idx];
142 }
143 if (r.kind === "node") {
144 out.push({
145 kind: "node",
146 valueIndex: r.valueIndex,
147 child: {
148 anchor: node,
149 state: null
150 }
151 });
152 } else if (r.kind === "attr") {
153 out.push({
154 kind: "attr",
155 element: node,
156 name: r.name,
157 template: r.template,
158 valueIndices: r.valueIndices
159 });
160 } else if (r.kind === "event") {
161 out.push({
162 kind: "event",
163 valueIndex: r.valueIndex,
164 element: node,
165 name: r.name
166 });
167 } else if (r.kind === "prop") {
168 out.push({
169 kind: "prop",
170 valueIndex: r.valueIndex,
171 element: node,
172 name: r.name
173 });
174 } else if (r.kind === "bool") {
175 out.push({
176 kind: "bool",
177 valueIndex: r.valueIndex,
178 element: node,
179 name: r.name
180 });
181 }
182 }
183 return out;
184 };
185 const entry = { template, buildParts };
186 compiledCache.set(strings, entry);
187 return entry;
188 }
189 const mountState = /* @__PURE__ */ new WeakMap();
190 function mountIntact(state, container) {
191 for (const node of state.nodes) {
192 if (node.parentNode !== container) {
193 return false;
194 }
195 }
196 return true;
197 }
198 function render(result, container) {
199 const existing = mountState.get(container);
200 if (existing && existing.strings === result.strings && mountIntact(existing, container)) {
201 applyValues(existing.parts, result.values);
202 return;
203 }
204 const compiled = compile(result.strings);
205 const fragment = compiled.template.content.cloneNode(true);
206 const parts = compiled.buildParts(fragment);
207 const nodes = Array.from(fragment.childNodes);
208 while (container.firstChild) {
209 container.removeChild(container.firstChild);
210 }
211 container.appendChild(fragment);
212 applyValues(parts, result.values);
213 mountState.set(container, { strings: result.strings, parts, nodes });
214 }
215 function applyValues(parts, values) {
216 for (const part of parts) {
217 if (part.kind === "node") {
218 updateChildPart(part.child, values[part.valueIndex]);
219 } else if (part.kind === "attr") {
220 let composed = part.template[0];
221 for (let i = 0; i < part.valueIndices.length; i++) {
222 composed += formatText(values[part.valueIndices[i]]);
223 composed += part.template[i + 1];
224 }
225 if (composed !== part.last) {
226 part.last = composed;
227 if (composed === "") {
228 part.element.removeAttribute(part.name);
229 } else {
230 part.element.setAttribute(part.name, composed);
231 }
232 }
233 } else if (part.kind === "event") {
234 const next = values[part.valueIndex];
235 if (next !== part.current) {
236 if (part.current) {
237 part.element.removeEventListener(part.name, part.current);
238 }
239 if (next) {
240 part.element.addEventListener(part.name, next);
241 }
242 part.current = next;
243 }
244 } else if (part.kind === "prop") {
245 const next = values[part.valueIndex];
246 if (next !== part.last) {
247 part.last = next;
248 part.element[part.name] = next;
249 }
250 } else if (part.kind === "bool") {
251 const next = !!values[part.valueIndex];
252 if (next !== part.last) {
253 part.last = next;
254 if (next) {
255 part.element.setAttribute(part.name, "");
256 } else {
257 part.element.removeAttribute(part.name);
258 }
259 }
260 }
261 }
262 }
263 function updateChildPart(child, value) {
264 if (value === null || value === void 0 || value === false) {
265 if (child.state) {
266 disposeChildState(child.state);
267 child.state = null;
268 }
269 return;
270 }
271 if (Array.isArray(value)) {
272 updateArrayChild(child, value);
273 return;
274 }
275 if (isTemplateResult(value)) {
276 updateTemplateChild(child, value);
277 return;
278 }
279 if (value instanceof Node) {
280 updateNodeChild(child, value);
281 return;
282 }
283 updateTextChild(child, formatText(value));
284 }
285 function updateNodeChild(child, node) {
286 const old = child.state;
287 if (old?.shape === "node" && old.node === node) {
288 return;
289 }
290 if (old) {
291 disposeChildState(old);
292 }
293 insertBeforeAnchor(child, [node]);
294 child.state = { shape: "node", node };
295 }
296 function updateTextChild(child, text) {
297 const old = child.state;
298 if (old?.shape === "text") {
299 if (old.text !== text) {
300 old.node.textContent = text;
301 old.text = text;
302 }
303 return;
304 }
305 if (old) {
306 disposeChildState(old);
307 }
308 const node = document.createTextNode(text);
309 insertBeforeAnchor(child, [node]);
310 child.state = { shape: "text", node, text };
311 }
312 function updateTemplateChild(child, result) {
313 const old = child.state;
314 if (old?.shape === "template" && old.strings === result.strings) {
315 applyValues(old.parts, result.values);
316 return;
317 }
318 if (old) {
319 disposeChildState(old);
320 }
321 const compiled = compile(result.strings);
322 const fragment = compiled.template.content.cloneNode(true);
323 const parts = compiled.buildParts(fragment);
324 const topNodes = Array.from(fragment.childNodes);
325 insertBeforeAnchor(child, [fragment]);
326 applyValues(parts, result.values);
327 child.state = {
328 shape: "template",
329 strings: result.strings,
330 parts,
331 nodes: topNodes
332 };
333 }
334 function updateArrayChild(child, arr) {
335 const old = child.state;
336 if (old?.shape === "array" && old.entries.length === arr.length) {
337 for (let i = 0; i < arr.length; i++) {
338 updateChildPart(old.entries[i], arr[i]);
339 }
340 return;
341 }
342 if (old) {
343 disposeChildState(old);
344 }
345 const entries = [];
346 for (const v of arr) {
347 const entryAnchor = document.createTextNode("");
348 insertBeforeAnchor(child, [entryAnchor]);
349 const entry = { anchor: entryAnchor, state: null };
350 updateChildPart(entry, v);
351 entries.push(entry);
352 }
353 child.state = { shape: "array", entries };
354 }
355 function insertBeforeAnchor(child, nodes) {
356 const parent = child.anchor.parentNode;
357 if (!parent) {
358 return;
359 }
360 for (const node of nodes) {
361 parent.insertBefore(node, child.anchor);
362 }
363 }
364 function disposeChildState(state) {
365 if (state.shape === "text") {
366 state.node.remove();
367 return;
368 }
369 if (state.shape === "template") {
370 for (const node of state.nodes) {
371 if (node.parentNode) {
372 node.parentNode.removeChild(node);
373 }
374 }
375 return;
376 }
377 if (state.shape === "node") {
378 if (state.node.parentNode) {
379 state.node.parentNode.removeChild(state.node);
380 }
381 return;
382 }
383 for (const entry of state.entries) {
384 if (entry.state) {
385 disposeChildState(entry.state);
386 }
387 entry.anchor.remove();
388 }
389 }
390 function formatText(v) {
391 if (v === null || v === void 0 || v === false) {
392 return "";
393 }
394 return String(v);
395 }
396 const _Component = class _Component extends HTMLElement {
397 constructor() {
398 super();
399 this._renderScheduled = false;
400 this._propValues = {};
401 const ctor = this.constructor;
402 if (ctor.shadow) {
403 this.attachShadow({ mode: "open" });
404 this._renderRoot = this.shadowRoot;
405 } else {
406 this._renderRoot = this;
407 }
408 this._installPropAccessors();
409 }
410 static get observedAttributes() {
411 return this.props.map(kebab);
412 }
413 connectedCallback() {
414 this._adoptStyles();
415 this.requestUpdate();
416 }
417 attributeChangedCallback(name, oldValue, newValue) {
418 if (oldValue === newValue) {
419 return;
420 }
421 const prop = camel(name);
422 this._propValues[prop] = newValue;
423 this.requestUpdate();
424 }
425 /**
426 * Declarative class-name setter. Assign an array (or a
427 * space-separated string) and the host's `class` attribute is
428 * rewritten to match. Intended for programmatic styling — when
429 * a plugin has enqueued its own stylesheet and wants to apply
430 * one of those classes to a shell component:
431 *
432 * ```js
433 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
434 * // → <wpd-select class="my-plugin-brand is-active">
435 * ```
436 *
437 * The plain HTML `class="…"` attribute works just the same and
438 * is always preferred when writing markup by hand — this setter
439 * exists for the JS-API case where the caller has an array of
440 * conditional classes in hand.
441 *
442 * Getter returns the current `classList` as a plain array for
443 * symmetric read/write.
444 *
445 * @since 0.5.0
446 */
447 get classNames() {
448 return Array.from(this.classList);
449 }
450 set classNames(next) {
451 if (next === null || next === void 0) {
452 this.removeAttribute("class");
453 return;
454 }
455 const list = Array.isArray(next) ? next : String(next).split(/\s+/);
456 const cleaned = list.map((s) => String(s).trim()).filter((s) => s !== "");
457 this.className = cleaned.join(" ");
458 }
459 /**
460 * Request a re-render explicitly. Components rarely need this —
461 * declare state via props + attribute observers and the render
462 * loop picks up changes automatically.
463 */
464 requestUpdate() {
465 this._scheduleRender();
466 }
467 /**
468 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
469 * by default (matches typical WC UX — events cross shadow
470 * boundaries, parents can listen without knowing about internal
471 * structure).
472 */
473 emit(name, detail) {
474 return this.dispatchEvent(
475 new CustomEvent(name, {
476 detail,
477 bubbles: true,
478 composed: true
479 })
480 );
481 }
482 // ------------------------------------------------------------------
483 // Internals
484 // ------------------------------------------------------------------
485 /**
486 * Wire every `static props` entry to a matched property getter +
487 * setter on the element. Setting the property reflects into the
488 * attribute (so downstream observers + CSS selectors see it);
489 * reading the property falls back to the attribute.
490 */
491 _installPropAccessors() {
492 const ctor = this.constructor;
493 for (const prop of ctor.props) {
494 if (Object.getOwnPropertyDescriptor(this, prop)) {
495 continue;
496 }
497 const attr = kebab(prop);
498 Object.defineProperty(this, prop, {
499 get: () => {
500 if (prop in this._propValues) {
501 return this._propValues[prop];
502 }
503 return this.getAttribute(attr);
504 },
505 set: (value) => {
506 let str;
507 if (value === null || value === void 0 || value === false) {
508 str = null;
509 } else if (value === true) {
510 str = "";
511 } else {
512 str = String(value);
513 }
514 this._propValues[prop] = str;
515 if (str === null) {
516 this.removeAttribute(attr);
517 } else {
518 this.setAttribute(attr, str);
519 }
520 this.requestUpdate();
521 },
522 enumerable: true,
523 configurable: true
524 });
525 }
526 }
527 /**
528 * Schedule a render on the next microtask. Multiple property
529 * assignments in the same tick collapse into a single render.
530 */
531 _scheduleRender() {
532 if (this._renderScheduled || !this.isConnected) {
533 return;
534 }
535 this._renderScheduled = true;
536 queueMicrotask(() => {
537 this._renderScheduled = false;
538 if (!this.isConnected) {
539 return;
540 }
541 render(this.render(), this._renderRoot);
542 });
543 }
544 /**
545 * Mount adoptable stylesheets onto the shadow root (via
546 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
547 * tag per def). No-op if `static styles` is empty.
548 */
549 _adoptStyles() {
550 const ctor = this.constructor;
551 if (ctor.styles.length === 0) {
552 return;
553 }
554 if (ctor.shadow && this.shadowRoot) {
555 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
556 this.shadowRoot.adoptedStyleSheets = sheets;
557 if (sheets.length !== ctor.styles.length) {
558 for (const s of ctor.styles) {
559 if (!s.sheet) {
560 const tag = document.createElement("style");
561 tag.textContent = s.cssText;
562 this.shadowRoot.appendChild(tag);
563 }
564 }
565 }
566 } else {
567 this._adoptLightStyles(ctor);
568 }
569 }
570 _adoptLightStyles(ctor) {
571 if (_Component._lightStylesAdopted.has(ctor)) {
572 return;
573 }
574 _Component._lightStylesAdopted.add(ctor);
575 for (const s of ctor.styles) {
576 const tag = document.createElement("style");
577 tag.dataset.wpdUi = this.tagName.toLowerCase();
578 tag.textContent = s.cssText;
579 document.head.appendChild(tag);
580 }
581 }
582 };
583 _Component.props = [];
584 _Component.styles = [];
585 _Component.shadow = true;
586 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
587 let Component = _Component;
588 function defineComponent(tag, ctor) {
589 if (customElements.get(tag)) {
590 return;
591 }
592 customElements.define(tag, ctor);
593 }
594 function kebab(s) {
595 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
596 }
597 function camel(s) {
598 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
599 }
600 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
601 try {
602 const s = new CSSStyleSheet();
603 return typeof s.replaceSync === "function";
604 } catch {
605 return false;
606 }
607 })();
608 function css(strings, ...values) {
609 let text = strings[0];
610 for (let i = 1; i < strings.length; i++) {
611 const v = values[i - 1];
612 if (typeof v === "string" || typeof v === "number") {
613 text += String(v);
614 } else if (v && v.__wpdCss) {
615 text += v.cssText;
616 } else {
617 throw new TypeError(
618 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
619 );
620 }
621 text += strings[i];
622 }
623 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
624 const sheet = new CSSStyleSheet();
625 sheet.replaceSync(text);
626 return { __wpdCss: true, sheet, cssText: text };
627 }
628 return { __wpdCss: true, sheet: null, cssText: text };
629 }
630 function computeAutoId(element) {
631 const parts = [];
632 const tabs = [];
633 let windowId = null;
634 let node = element.parentElement;
635 while (node) {
636 if (node === document.body || node === document.documentElement) {
637 break;
638 }
639 const id = node.id || "";
640 if (id.startsWith("wp-window-")) {
641 windowId = id.slice("wp-window-".length);
642 break;
643 }
644 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
645 const forValue = node.getAttribute("for");
646 if (forValue) {
647 tabs.unshift(forValue);
648 }
649 }
650 node = node.parentElement;
651 }
652 if (windowId) {
653 parts.push(slugify(windowId));
654 }
655 for (const tab of tabs) {
656 parts.push("tab-" + slugify(tab));
657 }
658 const label = element.getAttribute("label");
659 if (label) {
660 parts.push(slugify(label));
661 }
662 if (parts.length === 0) {
663 return "wpd-unnamed";
664 }
665 return "wpd-" + parts.filter((p) => p !== "").join("-");
666 }
667 function slugify(s) {
668 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
669 }
670 function ensureAutoId(element) {
671 if (element.id) {
672 return element.id;
673 }
674 const id = computeAutoId(element);
675 element.id = id;
676 return id;
677 }
678 const TEXT_DOMAIN = "desktop-mode";
679 function i18n() {
680 return window.wp?.i18n;
681 }
682 function __(text, domain = TEXT_DOMAIN) {
683 return i18n()?.__(text, domain) ?? text;
684 }
685 const containerStyles = css`:host{position:fixed;top:calc( var( --wp-admin--admin-bar--height,32px ) + 16px );inset-inline-end:16px;display:flex;flex-direction:column;gap:8px;z-index:calc( var( --desktop-mode-z-fullscreen,99999 ) + 10 );pointer-events:none}`;
686 const toastStyles = css`:host{display:flex;align-items:center;gap:12px;min-width:280px;max-width:420px;padding:10px 14px;background:#1d2327;color:#fff;border-radius:10px;border:1px solid rgba( 255,255,255,0.12 );box-shadow:0 10px 30px rgba( 0,0,0,0.4 ),0 2px 6px rgba( 0,0,0,0.18 ),inset 0 0 0 1px rgba( 255,255,255,0.04 );font-size:13px;line-height:1.4;opacity:0;transform:translateY( -8px );transition:opacity 0.18s ease,transform 0.18s ease;pointer-events:auto}:host( [ state='in' ] ){opacity:1;transform:translateY( 0 )}:host( [ state='out' ] ){opacity:0;transform:translateY( -8px )}.wpd-toast__label{flex:1}button[ hidden ]{display:none}button{flex-shrink:0;padding:4px 10px;border:none;border-radius:4px;background:rgba( 255,255,255,0.12 );color:#fff;font:inherit;font-size:12px;font-weight:500;cursor:pointer;transition:background-color 0.12s ease}button:hover{background:rgba( 255,255,255,0.22 )}button:focus-visible{outline:2px solid rgba( 255,255,255,0.6 );outline-offset:2px}.wpd-toast__close{display:inline-flex;align-items:center;justify-content:center;padding:4px;border-radius:6px;background:transparent;color:rgba( 255,255,255,0.7 )}.wpd-toast__close:hover{background:rgba( 255,255,255,0.14 );color:#fff}@media ( prefers-reduced-motion:reduce ){:host{transition-duration:0.01ms}}`;
687 const _WpdToastContainer = class _WpdToastContainer extends Component {
688 connectedCallback() {
689 super.connectedCallback();
690 this.setAttribute("aria-live", "polite");
691 }
692 render() {
693 return html`<slot></slot>`;
694 }
695 };
696 _WpdToastContainer.styles = [containerStyles];
697 _WpdToastContainer.help = {
698 title: "Toast container",
699 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
700 status: "stable",
701 since: "0.9.0",
702 slots: [
703 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
704 ],
705 cssProps: [
706 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
707 ],
708 example: html`
709 <wpd-toast-container>
710 <wpd-toast state="in">Settings saved.</wpd-toast>
711 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
712 </wpd-toast-container>
713 `
714 };
715 let WpdToastContainer = _WpdToastContainer;
716 defineComponent("wpd-toast-container", WpdToastContainer);
717 const _WpdToast = class _WpdToast extends Component {
718 connectedCallback() {
719 super.connectedCallback();
720 if (!this.hasAttribute("role")) {
721 this.setAttribute("role", "status");
722 }
723 }
724 render() {
725 const action = this.action || "";
726 const dismissible = this.hasAttribute("dismissible");
727 return html`
728 <span class="wpd-toast__label"><slot></slot></span>
729 <button
730 type="button"
731 ?hidden=${!action}
732 @click=${(e) => this._onAction(e)}
733 >
734 ${action}
735 </button>
736 <button
737 type="button"
738 class="wpd-toast__close"
739 aria-label=${__("Dismiss")}
740 ?hidden=${!dismissible}
741 @click=${(e) => this._onDismiss(e)}
742 >
743 <svg viewBox="0 0 14 14" width="12" height="12" aria-hidden="true" focusable="false">
744 <path
745 d="M3 3 L11 11 M11 3 L3 11"
746 stroke="currentColor"
747 stroke-width="1.7"
748 stroke-linecap="round"
749 fill="none"
750 ></path>
751 </svg>
752 </button>
753 `;
754 }
755 _onAction(e) {
756 e.preventDefault();
757 e.stopPropagation();
758 this.emit("wpd-toast-action", {});
759 }
760 _onDismiss(e) {
761 e.preventDefault();
762 e.stopPropagation();
763 this.emit("wpd-toast-dismiss", {});
764 }
765 };
766 _WpdToast.props = ["action", "state", "dismissible"];
767 _WpdToast.styles = [toastStyles];
768 _WpdToast.help = {
769 title: "Toast",
770 summary: 'Single transient notification. Message is slotted; fade-in / fade-out is CSS-driven by flipping the state attribute between "in" and "out". Usually created via the showToast() helper rather than authored by hand.',
771 status: "stable",
772 since: "0.9.0",
773 props: [
774 {
775 name: "action",
776 type: "string",
777 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
778 },
779 {
780 name: "state",
781 type: "'in' | 'out'",
782 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
783 },
784 {
785 name: "dismissible",
786 type: "boolean",
787 description: "When set, a close (×) button renders on the right and emits wpd-toast-dismiss on click. Use for persistent toasts the user must be able to close."
788 }
789 ],
790 slots: [
791 { name: "(default)", description: "Message text." }
792 ],
793 events: [
794 {
795 name: "wpd-toast-action",
796 description: "Fires when the action button is clicked.",
797 detail: "{}"
798 },
799 {
800 name: "wpd-toast-dismiss",
801 description: "Fires when the close (×) button is clicked.",
802 detail: "{}"
803 }
804 ],
805 example: html`
806 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
807 `
808 };
809 let WpdToast = _WpdToast;
810 defineComponent("wpd-toast", WpdToast);
811 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 )}`;
812 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
813 constructor() {
814 super(...arguments);
815 this._onKey = (e) => {
816 if (e.key === "Escape") {
817 e.preventDefault();
818 this._cancel();
819 }
820 if (e.key === "Enter" && !e.isComposing) {
821 e.preventDefault();
822 this._confirm();
823 }
824 };
825 this._onBackdrop = (e) => {
826 const path = e.composedPath();
827 const original = path.length > 0 ? path[0] : e.target;
828 if (original === this) {
829 this._cancel();
830 }
831 };
832 this._confirm = () => {
833 this.emit("wpd-confirm", { confirmed: true });
834 this.removeAttribute("open");
835 };
836 this._cancel = () => {
837 this.emit("wpd-cancel", { confirmed: false });
838 this.removeAttribute("open");
839 };
840 }
841 connectedCallback() {
842 super.connectedCallback();
843 this.setAttribute("role", "dialog");
844 this.setAttribute("aria-modal", "true");
845 this.addEventListener("keydown", this._onKey);
846 this.addEventListener("click", this._onBackdrop);
847 }
848 disconnectedCallback() {
849 this.removeEventListener("keydown", this._onKey);
850 this.removeEventListener("click", this._onBackdrop);
851 }
852 render() {
853 const title = this.title ?? "";
854 const message = this.message ?? "";
855 const confirmLabel = this["confirm-label"] || "Confirm";
856 const cancelLabel = this["cancel-label"] || "Cancel";
857 const isDanger = this.hasAttribute("danger");
858 const hideCancel = this.hasAttribute("hide-cancel");
859 const isDismissable = this.hasAttribute("dismissable");
860 return html`
861 <div class="dialog" tabindex="-1">
862 ${isDismissable ? html`<button
863 type="button"
864 class="close"
865 aria-label="Close"
866 @click=${() => this._cancel()}
867 >&times;</button>` : html``}
868 ${title ? html`<h2 class="title">${title}</h2>` : html``}
869 ${message ? html`<p class="message">${message}</p>` : html``}
870 <div class="actions">
871 ${hideCancel ? html`` : html`<button
872 type="button"
873 class="btn btn--secondary"
874 @click=${() => this._cancel()}
875 >
876 ${cancelLabel}
877 </button>`}
878 <button
879 type="button"
880 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
881 @click=${() => this._confirm()}
882 >
883 ${confirmLabel}
884 </button>
885 </div>
886 </div>
887 `;
888 }
889 };
890 _WpdConfirmDialog.props = [
891 "open",
892 "title",
893 "message",
894 "confirm-label",
895 "cancel-label",
896 "danger",
897 "hide-cancel",
898 "dismissable"
899 ];
900 _WpdConfirmDialog.styles = [dialogStyles];
901 _WpdConfirmDialog.help = {
902 title: "Confirm dialog",
903 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.",
904 status: "experimental",
905 since: "0.9.0",
906 props: [
907 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
908 { name: "title", type: "string", description: "Heading shown at the top." },
909 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
910 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
911 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
912 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
913 { 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." },
914 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
915 ],
916 events: [
917 {
918 name: "wpd-confirm",
919 description: "Fires on confirm. Detail: `{ confirmed: true }`."
920 },
921 {
922 name: "wpd-cancel",
923 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
924 }
925 ]
926 };
927 let WpdConfirmDialog = _WpdConfirmDialog;
928 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
929 const menuStyles$1 = css`:host{display:none;position:fixed;min-width:180px;background:var( --wpd-context-menu-bg,var( --desktop-mode-bg,#1d2327 ) );color:var( --wpd-context-menu-fg,var( --desktop-mode-fg,#fff ) );border:1px solid rgba( 255,255,255,0.08 );border-radius:8px;box-shadow:0 8px 24px rgba( 0,0,0,0.45 );padding:4px;font-size:13px;line-height:1.3;z-index:9999}:host( [ open ] ){display:block}`;
930 const optionStyles$1 = css`:host{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border:0;background:transparent;color:inherit;text-align:start;cursor:pointer;border-radius:4px;box-sizing:border-box;user-select:none}:host(:hover ),:host( [ active ] ){background:rgba( 255,255,255,0.1 );outline:none}:host( [ disabled ] ){opacity:0.45;cursor:not-allowed}:host( [ danger ] ){color:#ff8a8a}:host( [ danger ]:hover ){background:rgba( 255,90,90,0.18 )}:host( [ heading ] ){padding:8px 10px 4px;font-size:10px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:var( --wpd-context-menu-fg-muted,rgba( 255,255,255,0.5 ) );pointer-events:none}.icon{display:inline-flex;align-items:center;justify-content:center;font-size:18px;width:20px;height:20px}.label{flex:1}.chevron{margin-inline-start:auto;padding-inline-start:8px;font-size:16px;line-height:1;opacity:0.7}.check{display:inline-flex;align-items:center;justify-content:center;width:14px;font-size:13px;line-height:1;opacity:0.95}`;
931 const _WpdContextMenu = class _WpdContextMenu extends Component {
932 render() {
933 return html`
934 <slot></slot>
935 `;
936 }
937 connectedCallback() {
938 super.connectedCallback();
939 this.setAttribute("role", "menu");
940 }
941 };
942 _WpdContextMenu.props = ["open"];
943 _WpdContextMenu.styles = [menuStyles$1];
944 _WpdContextMenu.help = {
945 title: "Context menu",
946 summary: "Floating popup menu primitive. Pair with <wpd-context-menu-option> children. Toggle via the `open` boolean attribute. Listen for `wpd-context-menu-pick` to handle activation.",
947 status: "experimental",
948 since: "0.9.0",
949 props: [
950 {
951 name: "open",
952 type: "boolean attribute",
953 description: "Mounts the menu in its open / visible state."
954 }
955 ],
956 slots: [
957 { name: "(default)", description: "List of <wpd-context-menu-option> items." }
958 ],
959 events: [
960 {
961 name: "wpd-context-menu-pick",
962 description: "Bubbled from a non-disabled, non-heading option on activation. Detail: `{ id, value }`."
963 }
964 ]
965 };
966 let WpdContextMenu = _WpdContextMenu;
967 defineComponent("wpd-context-menu", WpdContextMenu);
968 const _WpdContextMenuOption = class _WpdContextMenuOption extends Component {
969 constructor() {
970 super(...arguments);
971 this._onActivate = (e) => {
972 if (this.hasAttribute("disabled") || this.hasAttribute("heading")) {
973 return;
974 }
975 const target = e.target;
976 if (target && target !== this && target.closest("wpd-context-menu-option") !== this) {
977 return;
978 }
979 this.emit("wpd-context-menu-pick", {
980 id: this.dataset.menuItemId ?? this.id ?? "",
981 value: this.getAttribute("value") ?? ""
982 });
983 };
984 this._onKey = (e) => {
985 if (e.key === "Enter" || e.key === " ") {
986 e.preventDefault();
987 this._onActivate(e);
988 }
989 };
990 }
991 connectedCallback() {
992 super.connectedCallback();
993 const isHeading = this.hasAttribute("heading");
994 this.setAttribute("role", isHeading ? "presentation" : "menuitem");
995 if (!isHeading) {
996 this.setAttribute("tabindex", "0");
997 }
998 this.addEventListener("click", this._onActivate);
999 this.addEventListener("keydown", this._onKey);
1000 }
1001 disconnectedCallback() {
1002 this.removeEventListener("click", this._onActivate);
1003 this.removeEventListener("keydown", this._onKey);
1004 }
1005 render() {
1006 const icon = this.getAttribute("icon");
1007 const hasChildren = this.hasAttribute("has-children");
1008 const checked = this.hasAttribute("checked");
1009 return html`
1010 ${checked ? html`<span class="check" aria-hidden="true">✓</span>` : html``}
1011 ${icon ? html`<span class="icon dashicons ${icon}" aria-hidden="true"></span>` : html``}
1012 <span class="label"><slot></slot></span>
1013 ${hasChildren ? html`<span class="chevron" aria-hidden="true">›</span>` : html``}
1014 `;
1015 }
1016 };
1017 _WpdContextMenuOption.props = [
1018 "value",
1019 "icon",
1020 "disabled",
1021 "danger",
1022 "heading",
1023 "has-children",
1024 "checked"
1025 ];
1026 _WpdContextMenuOption.styles = [optionStyles$1];
1027 _WpdContextMenuOption.help = {
1028 title: "Context menu option",
1029 summary: "Single row inside <wpd-context-menu>. Use `icon` for a leading dashicon, `danger` for destructive items, `heading` for a non-interactive section header, `has-children` to render a trailing chevron.",
1030 status: "experimental",
1031 since: "0.9.0",
1032 props: [
1033 {
1034 name: "value",
1035 type: "string",
1036 description: "Forwarded as `detail.value` on activation."
1037 },
1038 {
1039 name: "icon",
1040 type: "string",
1041 description: "Dashicon class (e.g. `dashicons-trash`)."
1042 },
1043 {
1044 name: "disabled",
1045 type: "boolean attribute",
1046 description: "Renders the option dimmed; clicks are ignored."
1047 },
1048 {
1049 name: "danger",
1050 type: "boolean attribute",
1051 description: "Destructive styling — red text, red hover."
1052 },
1053 {
1054 name: "heading",
1055 type: "boolean attribute",
1056 description: "Non-interactive section header. Ignores clicks."
1057 },
1058 {
1059 name: "has-children",
1060 type: "boolean attribute",
1061 description: "Renders a trailing chevron to suggest a submenu."
1062 },
1063 {
1064 name: "checked",
1065 type: "boolean attribute",
1066 description: "Renders a leading check mark — for radio-style picks inside a submenu (e.g. the active Sort By order)."
1067 }
1068 ],
1069 slots: [
1070 { name: "(default)", description: "Visible label." }
1071 ],
1072 events: [
1073 {
1074 name: "wpd-context-menu-pick",
1075 description: "Bubbled on click / Enter for non-heading non-disabled options. Detail: `{ id, value }`."
1076 }
1077 ]
1078 };
1079 let WpdContextMenuOption = _WpdContextMenuOption;
1080 defineComponent("wpd-context-menu-option", WpdContextMenuOption);
1081 const menuStyles = css`:host{display:block;min-width:220px;padding:4px;background:var( --desktop-mode-window-bg,#fff );color:var( --desktop-mode-text,#1d2327 );border:1px solid var( --desktop-mode-window-border,#c3c4c7 );border-radius:8px;box-shadow:0 8px 24px rgba( 0,0,0,0.18 ),0 2px 6px rgba( 0,0,0,0.08 )}:host( [ hidden ] ){display:none}`;
1082 const menuItemStyles = css`:host{display:block}button{display:flex;align-items:center;gap:10px;width:100%;min-height:32px;padding:6px 10px;border:none;border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:1.3;text-align:start;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease}button:hover,button:focus-visible{background:rgba( 0,0,0,0.06 );color:#000;outline:none}button:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-2px}.wpd-menu-item__icon{flex-shrink:0;width:18px;height:18px;font-size:18px;line-height:1;color:var( --wp-admin-theme-color,#2271b1 )}.wpd-menu-item__icon[ hidden ]{display:none}.wpd-menu-item__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wpd-menu-item__check{flex-shrink:0;width:16px;height:16px;border-radius:3px;border:1.5px solid rgba( 0,0,0,0.25 );position:relative;background:transparent;transition:background-color 0.12s ease,border-color 0.12s ease}.wpd-menu-item__check[ hidden ]{display:none}:host( [ checked ] ) .wpd-menu-item__check{background:var( --wp-admin-theme-color,#2271b1 );border-color:var( --wp-admin-theme-color,#2271b1 )}:host( [ checked ] ) .wpd-menu-item__check::after{content:'';position:absolute;top:1px;left:4px;width:4px;height:8px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate( 45deg )}`;
1083 const _WpdMenu = class _WpdMenu extends Component {
1084 connectedCallback() {
1085 super.connectedCallback();
1086 this.setAttribute("role", "menu");
1087 }
1088 render() {
1089 return html`<slot></slot>`;
1090 }
1091 };
1092 _WpdMenu.styles = [menuStyles];
1093 _WpdMenu.help = {
1094 title: "Menu",
1095 summary: "Popover menu used in window title bars and other overflow triggers. Presentation-only: the consumer owns open/close state via the `hidden` attribute and any outside-click dismissal.",
1096 status: "stable",
1097 since: "0.9.0",
1098 slots: [
1099 { name: "(default)", description: "<wpd-menu-item> children." }
1100 ],
1101 cssProps: [
1102 { name: "--desktop-mode-window-bg", description: "Menu background." },
1103 { name: "--desktop-mode-window-border", description: "Menu border." },
1104 { name: "--desktop-mode-text", description: "Item text colour." }
1105 ],
1106 example: html`
1107 <wpd-menu>
1108 <wpd-menu-item value="new" icon="dashicons-plus">Open another window</wpd-menu-item>
1109 <wpd-menu-item value="startup" role="menuitemcheckbox" checked>Open on startup</wpd-menu-item>
1110 <wpd-menu-item value="close">Close window</wpd-menu-item>
1111 </wpd-menu>
1112 `
1113 };
1114 let WpdMenu = _WpdMenu;
1115 defineComponent("wpd-menu", WpdMenu);
1116 const _WpdMenuItem = class _WpdMenuItem extends Component {
1117 connectedCallback() {
1118 super.connectedCallback();
1119 if (!this.hasAttribute("role")) {
1120 this.setAttribute("role", "menuitem");
1121 }
1122 }
1123 render() {
1124 const icon = this.icon || "";
1125 const isCheckbox = this.getAttribute("role") === "menuitemcheckbox";
1126 const checked = this.checked !== null;
1127 if (isCheckbox) {
1128 this.setAttribute("aria-checked", checked ? "true" : "false");
1129 }
1130 return html`
1131 <button type="button" @click=${(e) => this._onPick(e)}>
1132 <span
1133 class="wpd-menu-item__check"
1134 ?hidden=${!isCheckbox}
1135 ></span>
1136 <span
1137 class="wpd-menu-item__icon dashicons ${icon}"
1138 aria-hidden="true"
1139 ?hidden=${isCheckbox || !icon}
1140 ></span>
1141 <span class="wpd-menu-item__label">
1142 <slot></slot>
1143 </span>
1144 </button>
1145 `;
1146 }
1147 _onPick(e) {
1148 e.preventDefault();
1149 this.emit("wpd-menu-item-click", {
1150 value: this.value
1151 });
1152 }
1153 };
1154 _WpdMenuItem.props = ["icon", "value", "checked"];
1155 _WpdMenuItem.styles = [menuItemStyles];
1156 _WpdMenuItem.help = {
1157 title: "Menu item",
1158 summary: 'Single row inside a <wpd-menu>. Supports three looks: plain label, left-aligned dashicon (icon="dashicons-…"), or a checkbox indicator (role="menuitemcheckbox" + checked).',
1159 status: "stable",
1160 since: "0.9.0",
1161 props: [
1162 {
1163 name: "icon",
1164 type: "string (dashicons class)",
1165 description: 'Dashicons class rendered on the left. Ignored when role="menuitemcheckbox".'
1166 },
1167 {
1168 name: "value",
1169 type: "string",
1170 description: "Identifier emitted in wpd-menu-item-click.detail.value."
1171 },
1172 {
1173 name: "checked",
1174 type: "boolean attribute",
1175 description: 'Visible check indicator. Only honoured when role="menuitemcheckbox".'
1176 }
1177 ],
1178 slots: [
1179 { name: "(default)", description: "Menu item label." }
1180 ],
1181 events: [
1182 {
1183 name: "wpd-menu-item-click",
1184 description: "Fires when the item is clicked; bubbles so the <wpd-menu> parent can delegate.",
1185 detail: "{ value: string | null }"
1186 }
1187 ]
1188 };
1189 let WpdMenuItem = _WpdMenuItem;
1190 defineComponent("wpd-menu-item", WpdMenuItem);
1191 const styles$4 = css`:host{display:inline-flex}button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:none;border-radius:5px;background:transparent;color:var( --wpd-btn-color,currentColor );cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease}button:hover{color:var( --wpd-btn-color-hover,currentColor );background:var( --wpd-btn-bg-hover,rgba( 0,0,0,0.06 ) )}button:focus-visible{color:var( --wpd-btn-color-hover,currentColor );background:var( --wpd-btn-bg-hover,rgba( 0,0,0,0.06 ) );outline:2px solid var( --wpd-btn-outline,currentColor );outline-offset:1px}:host( [ active ] ) button{color:var( --wpd-btn-color-hover,currentColor );background:var( --wpd-btn-bg-active,rgba( 0,0,0,0.08 ) )}:host( [ danger ] ) button:hover{color:#fff;background:var( --wpd-btn-danger-hover,#d63638 )}svg{display:block;pointer-events:none;flex-shrink:0}svg:empty{display:none}::slotted( span ){line-height:1}::slotted( svg ){display:block}`;
1192 const ICONS$1 = {
1193 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
1194 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
1195 fullscreen: '<path d="M4.5 2H2v2.5M10 4.5V2H7.5M4.5 10H2V7.5M10 7.5V10H7.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
1196 "fullscreen-exit": '<path d="M2 4.5H4.5V2M7.5 2V4.5H10M2 7.5H4.5V10M7.5 10V7.5H10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
1197 detach: '<path d="M5 2H2.5v7.5H10V7M6.5 2H10v3.5M10 2L5.5 6.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
1198 reload: (
1199 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
1200 // shared with the other title-bar glyphs. The wrapping `<g>` does
1201 // the math; the inner path is dropped in unmodified so its
1202 // authoring tool can be re-edited and copy-pasted again.
1203 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
1204 // keep the result centered inside the 12×12 viewBox so the
1205 // glyph reads slightly smaller than min/max/close — closer to
1206 // the visual weight of the other title-bar buttons.
1207 '<g transform="translate(0.6 0.6) scale(0.021)" fill="currentColor"><path d="m504.554 233.704-76.447 91.467c-6.329 7.572-15.417 11.479-24.571 11.479a31.872 31.872 0 0 1-20.504-7.447l-91.467-76.447c-13.561-11.334-15.366-31.515-4.032-45.075s31.515-15.366 45.075-4.032l37.506 31.347c-10.274-74.891-74.668-132.774-152.337-132.774C132.984 102.223 64 171.207 64 256s68.984 153.777 153.777 153.777c17.673 0 32 14.327 32 32s-14.327 32-32 32c-58.17 0-112.859-22.653-153.991-63.785C22.653 368.859 0 314.17 0 256s22.653-112.859 63.786-153.992c41.132-41.132 95.821-63.785 153.991-63.785s112.859 22.653 153.992 63.785c32.517 32.516 53.471 73.508 60.829 117.991l22.849-27.339c11.334-13.56 31.515-15.364 45.075-4.032 13.56 11.335 15.365 31.516 4.032 45.076z"/></g>'
1208 ),
1209 close: '<path d="M3.25 3.25l5.5 5.5M3.25 8.75l5.5-5.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
1210 menu: '<circle cx="3" cy="6" r="1.2" fill="currentColor"/><circle cx="6" cy="6" r="1.2" fill="currentColor"/><circle cx="9" cy="6" r="1.2" fill="currentColor"/>'
1211 };
1212 const _WpdWindowButton = class _WpdWindowButton extends Component {
1213 constructor() {
1214 super(...arguments);
1215 this._activateWired = false;
1216 }
1217 render() {
1218 const iconKey = this.icon || "";
1219 const svgInner = ICONS$1[iconKey] || "";
1220 return html`
1221 <button type="button">
1222 <svg
1223 width="14"
1224 height="14"
1225 viewBox="0 0 12 12"
1226 aria-hidden="true"
1227 focusable="false"
1228 ></svg>
1229 <slot></slot>
1230 </button>
1231 <span data-svg-buffer style="display:none">${svgInner}</span>
1232 `;
1233 }
1234 /**
1235 * After each render, copy the raw SVG markup into the actual
1236 * `<svg>` element. The templater only writes text into slots,
1237 * so we stash the intended markup in a hidden buffer and
1238 * `innerHTML = ` the svg once here — a one-shot post-render
1239 * hook that keeps the declarative template honest.
1240 *
1241 * Also wires up the `wpd-button-activate` CustomEvent that
1242 * fires exactly once per gesture — the canonical contract
1243 * for plugin-registered title-bar buttons. Plugin authors who
1244 * use `addEventListener( 'click', cb )` directly still get
1245 * what they expect (the title bar's drag-handler now excludes
1246 * chrome buttons by class so static clicks land normally),
1247 * but `wpd-button-activate` is the documented surface that
1248 * documents the once-per-gesture contract explicitly. See
1249 * the class-level docblock for rationale.
1250 */
1251 connectedCallback() {
1252 super.connectedCallback();
1253 queueMicrotask(() => this._paintSvg());
1254 queueMicrotask(() => this._wireActivateEvent());
1255 }
1256 attributeChangedCallback(name, oldValue, newValue) {
1257 super.attributeChangedCallback(name, oldValue, newValue);
1258 queueMicrotask(() => this._paintSvg());
1259 }
1260 _paintSvg() {
1261 const root = this.shadowRoot;
1262 if (!root) {
1263 return;
1264 }
1265 const svg = root.querySelector("svg");
1266 const buffer = root.querySelector("[data-svg-buffer]");
1267 if (svg && buffer) {
1268 const markup = buffer.textContent || "";
1269 if (svg.innerHTML !== markup) {
1270 svg.innerHTML = markup;
1271 }
1272 }
1273 }
1274 _wireActivateEvent() {
1275 if (this._activateWired) {
1276 return;
1277 }
1278 const root = this.shadowRoot;
1279 if (!root) {
1280 return;
1281 }
1282 const button = root.querySelector("button");
1283 if (!button) {
1284 return;
1285 }
1286 this._activateWired = true;
1287 button.addEventListener("click", () => {
1288 this.dispatchEvent(
1289 new CustomEvent("wpd-button-activate", {
1290 bubbles: true,
1291 composed: true,
1292 cancelable: true
1293 })
1294 );
1295 });
1296 }
1297 };
1298 _WpdWindowButton.props = ["icon", "active", "danger"];
1299 _WpdWindowButton.styles = [styles$4];
1300 _WpdWindowButton.help = {
1301 title: "Window button",
1302 summary: "Chrome button used in native-window title bars. Built-in icons cover the standard controls (minimize, maximize, fullscreen, detach, close, menu). Focused/unfocused coloring is driven by --wpd-btn-* CSS custom properties the window shell owns.",
1303 status: "stable",
1304 since: "0.9.0",
1305 props: [
1306 {
1307 name: "icon",
1308 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
1309 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
1310 },
1311 {
1312 name: "active",
1313 type: "boolean attribute",
1314 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
1315 },
1316 {
1317 name: "danger",
1318 type: "boolean attribute",
1319 description: "Swaps the hover wash to red — used by the close button."
1320 }
1321 ],
1322 slots: [
1323 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
1324 ],
1325 cssProps: [
1326 { name: "--wpd-btn-color", description: "Resting foreground." },
1327 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
1328 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
1329 { name: "--wpd-btn-bg-active", description: "Pressed background." },
1330 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
1331 { name: "--wpd-btn-outline", description: "Focus outline colour." }
1332 ],
1333 example: html`
1334 <wpd-cluster gap="2">
1335 <wpd-window-button icon="minimize"></wpd-window-button>
1336 <wpd-window-button icon="maximize"></wpd-window-button>
1337 <wpd-window-button icon="menu"></wpd-window-button>
1338 <wpd-window-button icon="close" danger></wpd-window-button>
1339 </wpd-cluster>
1340 `
1341 };
1342 let WpdWindowButton = _WpdWindowButton;
1343 defineComponent("wpd-window-button", WpdWindowButton);
1344 const styles$3 = css`:host{display:inline-flex}button{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;border-radius:4px;background:transparent;color:rgba( 0,0,0,0.45 );cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease,transform 0.12s ease}:host( [ variant='detach' ] ) button:hover{color:var( --wp-admin-theme-color,#2271b1 );background:rgba( 34,113,177,0.12 );transform:translateY( -1px )}:host( [ variant='detach' ] ) button:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:1px}:host( [ variant='close' ] ) button:hover{color:#fff;background:#d63638}:host( [ variant='close' ] ) button:focus-visible{color:#fff;background:#d63638;outline:2px solid rgba( 214,54,56,0.6 );outline-offset:1px}svg{display:block;pointer-events:none;width:12px;height:12px}@media ( prefers-reduced-motion:reduce ){button{transition-duration:0.01ms}:host( [ variant='detach' ] ) button:hover{transform:none}}`;
1345 const ICONS = {
1346 detach: '<path d="M5 2H2.5v7.5H10V7M6.5 2H10v3.5M10 2L5.5 6.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
1347 close: '<path d="M2.5 2.5l7 7M9.5 2.5l-7 7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>'
1348 };
1349 const _WpdTabChip = class _WpdTabChip extends Component {
1350 render() {
1351 const variant = this.variant || "";
1352 const svgInner = ICONS[variant] || "";
1353 return html`
1354 <button type="button">
1355 <svg
1356 viewBox="0 0 12 12"
1357 aria-hidden="true"
1358 focusable="false"
1359 ></svg>
1360 <slot></slot>
1361 </button>
1362 <span data-svg-buffer style="display:none">${svgInner}</span>
1363 `;
1364 }
1365 connectedCallback() {
1366 super.connectedCallback();
1367 queueMicrotask(() => this._paintSvg());
1368 }
1369 attributeChangedCallback(name, oldValue, newValue) {
1370 super.attributeChangedCallback(name, oldValue, newValue);
1371 queueMicrotask(() => this._paintSvg());
1372 }
1373 _paintSvg() {
1374 const root = this.shadowRoot;
1375 if (!root) {
1376 return;
1377 }
1378 const svg = root.querySelector("svg");
1379 const buffer = root.querySelector("[data-svg-buffer]");
1380 if (svg && buffer) {
1381 const markup = buffer.textContent || "";
1382 if (svg.innerHTML !== markup) {
1383 svg.innerHTML = markup;
1384 }
1385 }
1386 }
1387 };
1388 _WpdTabChip.props = ["variant"];
1389 _WpdTabChip.styles = [styles$3];
1390 _WpdTabChip.help = {
1391 title: "Tab chip",
1392 summary: "Small action button dropped inside an external sub-tab. `detach` lifts with an accent wash on hover; `close` uses a red destructive wash. Click bubbles as a native click — consumers read `variant` if they need to distinguish.",
1393 status: "stable",
1394 since: "0.9.0",
1395 props: [
1396 {
1397 name: "variant",
1398 type: "'detach' | 'close'",
1399 description: "Selects the built-in SVG icon and the hover wash colour."
1400 }
1401 ],
1402 slots: [
1403 { name: "(default)", description: "Optional custom icon markup when `variant` is omitted." }
1404 ],
1405 example: html`
1406 <wpd-cluster gap="4">
1407 <wpd-tab-chip variant="detach"></wpd-tab-chip>
1408 <wpd-tab-chip variant="close"></wpd-tab-chip>
1409 </wpd-cluster>
1410 `
1411 };
1412 let WpdTabChip = _WpdTabChip;
1413 defineComponent("wpd-tab-chip", WpdTabChip);
1414 const styles$2 = css`:host{display:inline-flex;align-items:center;gap:6px;font-size:var( --wpd-save-status-font-size,11px );line-height:1;color:var( --wpd-save-status-fg,currentColor );vertical-align:middle;min-width:0;opacity:1;pointer-events:auto}.wpd-save-status__indicator{display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;flex-shrink:0;box-sizing:border-box;background:var( --wpd-save-status-bg,transparent );border:2px solid var( --wpd-save-status-idle-color,color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 55%,transparent ) );color:var( --wp-admin-theme-color,#2271b1 );transition:background-color 0.2s ease,border-color 0.2s ease,box-shadow 0.2s ease}:host( [ phase='pending' ] ) .wpd-save-status__indicator,:host( [ phase='saving' ] ) .wpd-save-status__indicator{background:var( --wpd-save-status-bg,var( --wp-admin-theme-color,#2271b1 ) );border-color:transparent;color:var( --wp-admin-theme-color,#2271b1 );animation:wpd-save-status-pulse 1.2s ease-in-out infinite}:host( [ animation='modem' ][ phase='pending' ] ) .wpd-save-status__indicator,:host( [ animation='modem' ][ phase='saving' ] ) .wpd-save-status__indicator{background:var( --wpd-save-status-bg,var( --wp-admin-theme-color,#2271b1 ) );border-color:transparent;color:var( --wp-admin-theme-color,#2271b1 );animation:wpd-save-status-modem-stutter 1.8s ease-in-out infinite,wpd-save-status-modem-glow 2.4s ease-in-out infinite}@keyframes wpd-save-status-modem-stutter{0%,4%{opacity:1}5%,30%{opacity:0.22}31%,36%{opacity:1}37%,39%{opacity:0.22}40%,44%{opacity:1}45%,67%{opacity:0.22}68%,76%{opacity:1}77%,100%{opacity:0.22}}@keyframes wpd-save-status-modem-glow{0%,12%{box-shadow:0 0 0 0 transparent}13%,22%{box-shadow:0 0 4px 0 currentColor}23%,50%{box-shadow:0 0 0 0 transparent}51%,58%{box-shadow:0 0 4px 0 currentColor}59%,84%{box-shadow:0 0 0 0 transparent}85%,94%{box-shadow:0 0 5px 0 currentColor}95%,100%{box-shadow:0 0 0 0 transparent}}@media ( prefers-reduced-motion:reduce ){:host( [ phase='pending' ] ) .wpd-save-status__indicator,:host( [ phase='saving' ] ) .wpd-save-status__indicator,:host( [ animation='modem' ][ phase='pending' ] ) .wpd-save-status__indicator,:host( [ animation='modem' ][ phase='saving' ] ) .wpd-save-status__indicator{animation:none;opacity:0.85}}:host( [ phase='saved' ] ) .wpd-save-status__indicator{background:var( --wpd-save-status-saved-bg,#1d6f42 );border-color:transparent;color:var( --wpd-save-status-saved-bg,#1d6f42 )}:host( [ phase='failed' ] ) .wpd-save-status__indicator{background:var( --wpd-save-status-failed-bg,#d63638 );border-color:transparent;color:var( --wpd-save-status-failed-bg,#d63638 );animation:wpd-save-status-pulse 0.8s ease-in-out 2}@keyframes wpd-save-status-pulse{0%,100%{opacity:0.55;transform:scale( 0.9 )}50%{opacity:1;transform:scale( 1 )}}:host( [ mode='pill' ] ) .wpd-save-status{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:999px;background:var( --wpd-save-status-pill-bg,transparent );font-weight:500;white-space:nowrap}:host( [ mode='pill' ][ phase='saving' ] ) .wpd-save-status,:host( [ mode='pill' ][ phase='pending' ] ) .wpd-save-status{background:var( --wpd-save-status-pill-bg,rgba( 0,0,0,0.04 ) );color:var( --wpd-save-status-pill-fg,#50575e )}:host( [ mode='pill' ][ phase='saved' ] ) .wpd-save-status{background:var( --wpd-save-status-pill-bg,rgba( 30,132,73,0.12 ) );color:var( --wpd-save-status-pill-fg,#1d6f42 )}:host( [ mode='pill' ][ phase='failed' ] ) .wpd-save-status{background:var( --wpd-save-status-pill-bg,rgba( 214,54,56,0.12 ) );color:var( --wpd-save-status-pill-fg,#a02622 )}.wpd-save-status__label{min-width:0;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host( [ phase='saved' ] ) .wpd-save-status__glyph,:host( [ phase='failed' ] ) .wpd-save-status__glyph{display:inline-block;color:#fff;width:8px;height:8px}.wpd-save-status__glyph{display:none}.wpd-save-status__glyph svg{display:block;width:100%;height:100%}`;
1415 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
1416 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
1417 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
1418 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
1419 constructor() {
1420 super(...arguments);
1421 this._autoTimer = null;
1422 this._docListener = null;
1423 }
1424 connectedCallback() {
1425 super.connectedCallback();
1426 if (this.auto !== null) {
1427 this._installAutoListener();
1428 }
1429 }
1430 disconnectedCallback() {
1431 this._removeAutoListener();
1432 if (this._autoTimer !== null) {
1433 window.clearTimeout(this._autoTimer);
1434 this._autoTimer = null;
1435 }
1436 }
1437 attributeChangedCallback(name, oldValue, newValue) {
1438 super.attributeChangedCallback(name, oldValue, newValue);
1439 if (name === "auto" || name === "event") {
1440 this._removeAutoListener();
1441 if (this.auto !== null) {
1442 this._installAutoListener();
1443 }
1444 }
1445 if (name === "phase") {
1446 this._scheduleAutoClear();
1447 const detail = {
1448 phase: this.phase ?? "idle",
1449 error: this.error ?? void 0
1450 };
1451 this.emit("wpd-save-status-change", detail);
1452 }
1453 }
1454 render() {
1455 const phase = this.phase ?? "idle";
1456 const mode = this.mode ?? "dot";
1457 const error = this.error ?? "";
1458 const title = error || this._labelForPhase(phase);
1459 if (title) {
1460 this.setAttribute("title", title);
1461 } else {
1462 this.removeAttribute("title");
1463 }
1464 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
1465 this.setAttribute("role", phase === "failed" ? "alert" : "status");
1466 return html`
1467 <span class="wpd-save-status">
1468 <span class="wpd-save-status__indicator" aria-hidden="true">
1469 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
1470 </span>
1471 ${mode === "pill" ? html`<span class="wpd-save-status__label"
1472 >${this._labelForPhase(phase)}</span
1473 >` : html``}
1474 </span>
1475 `;
1476 }
1477 _renderGlyph(phase) {
1478 if (phase === "saved") {
1479 return _iconCheck();
1480 }
1481 if (phase === "failed") {
1482 return _iconBang();
1483 }
1484 return "";
1485 }
1486 _labelForPhase(phase) {
1487 switch (phase) {
1488 case "pending":
1489 case "saving":
1490 return this["saving-label"] ?? "Saving…";
1491 case "saved":
1492 return this["saved-label"] ?? "Saved";
1493 case "failed": {
1494 const err = this.error ?? "";
1495 return err || "Couldn’t save";
1496 }
1497 default:
1498 return this["idle-label"] ?? "";
1499 }
1500 }
1501 _installAutoListener() {
1502 const eventName = this.event || DEFAULT_EVENT;
1503 this._docListener = (e) => {
1504 const detail = e.detail;
1505 if (!detail || typeof detail.phase !== "string") {
1506 return;
1507 }
1508 this.phase = detail.phase;
1509 if (detail.error) {
1510 this.error = detail.error;
1511 } else if (detail.phase !== "failed" && this.error) {
1512 this.removeAttribute("error");
1513 }
1514 };
1515 document.addEventListener(eventName, this._docListener);
1516 }
1517 _removeAutoListener() {
1518 if (!this._docListener) {
1519 return;
1520 }
1521 const eventName = this.event || DEFAULT_EVENT;
1522 document.removeEventListener(eventName, this._docListener);
1523 this._docListener = null;
1524 }
1525 _scheduleAutoClear() {
1526 if (this._autoTimer !== null) {
1527 window.clearTimeout(this._autoTimer);
1528 this._autoTimer = null;
1529 }
1530 const phase = this.phase ?? "idle";
1531 const ms = this._autoClearMsFor(phase);
1532 if (ms <= 0) {
1533 return;
1534 }
1535 this._autoTimer = window.setTimeout(() => {
1536 this._autoTimer = null;
1537 this.phase = "idle";
1538 }, ms);
1539 }
1540 _autoClearMsFor(phase) {
1541 if (phase === "saved") {
1542 const raw = this["auto-clear-saved-ms"];
1543 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
1544 }
1545 if (phase === "failed") {
1546 const raw = this["auto-clear-failed-ms"];
1547 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
1548 }
1549 return 0;
1550 }
1551 };
1552 _WpdSaveStatus.props = [
1553 "phase",
1554 "mode",
1555 "animation",
1556 "auto",
1557 "event",
1558 "error",
1559 "saving-label",
1560 "saved-label",
1561 "idle-label",
1562 "auto-clear-saved-ms",
1563 "auto-clear-failed-ms"
1564 ];
1565 _WpdSaveStatus.styles = [styles$2];
1566 _WpdSaveStatus.help = {
1567 title: "Save status",
1568 summary: 'Tiny status indicator for "is this change saved yet?" affordances. Three layouts (dot / icon / pill), five phases, optional auto-listen to a save-lifecycle CustomEvent so every input in the panel inherits feedback for free.',
1569 status: "experimental",
1570 since: "0.8.0",
1571 props: [
1572 {
1573 name: "phase",
1574 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
1575 default: "idle",
1576 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
1577 },
1578 {
1579 name: "mode",
1580 type: "'dot' | 'icon' | 'pill'",
1581 default: "dot",
1582 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
1583 },
1584 {
1585 name: "animation",
1586 type: "'pulse' | 'modem'",
1587 default: "pulse",
1588 description: "Animation cadence during the saving phase. `pulse` (default) is a smooth ease-in-out; `modem` is an irregular activity-LED blink with a soft glow — suits a 'data-flowing' affordance in window title bars."
1589 },
1590 {
1591 name: "auto",
1592 type: "boolean attribute",
1593 description: 'Subscribe to a CustomEvent on `document` and populate phase + error from its detail. Default event name is `desktop-mode-os-settings-save-lifecycle`; override with `event="…"`.'
1594 },
1595 {
1596 name: "event",
1597 type: "string",
1598 default: "desktop-mode-os-settings-save-lifecycle",
1599 description: "CustomEvent name to listen on when `auto` is set."
1600 },
1601 {
1602 name: "error",
1603 type: "string",
1604 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
1605 },
1606 {
1607 name: "saving-label",
1608 type: "string",
1609 default: "Saving…",
1610 description: "Pill-mode label shown during `pending` / `saving`."
1611 },
1612 {
1613 name: "saved-label",
1614 type: "string",
1615 default: "Saved",
1616 description: "Pill-mode label shown during `saved`."
1617 },
1618 {
1619 name: "idle-label",
1620 type: "string",
1621 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
1622 },
1623 {
1624 name: "auto-clear-saved-ms",
1625 type: "integer",
1626 default: "2200",
1627 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
1628 },
1629 {
1630 name: "auto-clear-failed-ms",
1631 type: "integer",
1632 default: "6000",
1633 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
1634 }
1635 ],
1636 events: [
1637 {
1638 name: "wpd-save-status-change",
1639 description: "Fires when the phase changes (manually or via auto-listen).",
1640 detail: "{ phase, error }"
1641 }
1642 ],
1643 cssProps: [
1644 {
1645 name: "--wpd-save-status-bg",
1646 description: "Indicator background color (saving/pending phase)."
1647 },
1648 {
1649 name: "--wpd-save-status-saved-bg",
1650 description: "Indicator background on saved."
1651 },
1652 {
1653 name: "--wpd-save-status-failed-bg",
1654 description: "Indicator background on failed."
1655 },
1656 {
1657 name: "--wpd-save-status-pill-bg",
1658 description: "Pill background (mode=pill)."
1659 },
1660 {
1661 name: "--wpd-save-status-pill-fg",
1662 description: "Pill foreground (mode=pill)."
1663 }
1664 ],
1665 example: html`
1666 <wpd-cluster gap="12">
1667 <wpd-save-status phase="pending"></wpd-save-status>
1668 <wpd-save-status phase="saving"></wpd-save-status>
1669 <wpd-save-status phase="saved"></wpd-save-status>
1670 <wpd-save-status phase="failed"></wpd-save-status>
1671 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
1672 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
1673 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
1674 </wpd-cluster>
1675 `
1676 };
1677 let WpdSaveStatus = _WpdSaveStatus;
1678 defineComponent("wpd-save-status", WpdSaveStatus);
1679 function _iconCheck() {
1680 return html`
1681 <svg
1682 viewBox="0 0 12 12"
1683 aria-hidden="true"
1684 focusable="false"
1685 fill="none"
1686 stroke="currentColor"
1687 stroke-width="2"
1688 stroke-linecap="round"
1689 stroke-linejoin="round"
1690 >
1691 <path d="M2.5 6 L5 8.5 L9.5 4" />
1692 </svg>
1693 `;
1694 }
1695 function _iconBang() {
1696 return html`
1697 <svg
1698 viewBox="0 0 12 12"
1699 aria-hidden="true"
1700 focusable="false"
1701 fill="currentColor"
1702 >
1703 <path
1704 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
1705 />
1706 </svg>
1707 `;
1708 }
1709 const styles$1 = css`:host{display:inline-block;--wpd-spinner-color:var( --wp-admin-theme-color,#21759b );--wpd-spinner-accent:#fff;--wpd-spinner-size:48px;width:var( --wpd-spinner-size );height:var( --wpd-spinner-size );color:var( --wpd-spinner-color );vertical-align:middle;line-height:0}:host( [ hidden ] ){display:none}.root,.root svg{display:block;width:100%;height:100%}.root svg .mark{fill:var( --wpd-spinner-accent,#fff )}@keyframes wpd-spinner-spin{to{transform:rotate( 360deg )}}@keyframes wpd-spinner-scale{0%,100%{transform:scale( 1 )}50%{transform:scale( 1.045 )}}@keyframes wpd-spinner-opacity{0%,100%{opacity:1}50%{opacity:0.7}}@media ( prefers-reduced-motion:reduce ){.root svg [ style*='animation' ]{animation:none !important}}`;
1710 const WPD_SPINNER_PRESETS = Object.freeze({
1711 classic: {
1712 sp1: 12,
1713 sp2: 24,
1714 sp3: 40,
1715 a1: 28,
1716 a2: 15,
1717 a3: 8,
1718 gap: 4,
1719 dir2: 1,
1720 dir3: -1,
1721 pulse: "none",
1722 dots: 0
1723 },
1724 comet: {
1725 sp1: 8,
1726 sp2: 14,
1727 sp3: 26,
1728 a1: 50,
1729 a2: 28,
1730 a3: 12,
1731 gap: 3,
1732 dir2: 1,
1733 dir3: 1,
1734 pulse: "none",
1735 dots: 5
1736 },
1737 orbit: {
1738 sp1: 10,
1739 sp2: 10,
1740 sp3: 32,
1741 a1: 50,
1742 a2: 50,
1743 a3: 8,
1744 gap: 5,
1745 dir2: -1,
1746 dir3: -1,
1747 pulse: "opacity",
1748 dots: 3
1749 },
1750 pulse: {
1751 sp1: 6,
1752 sp2: 18,
1753 sp3: 30,
1754 a1: 20,
1755 a2: 12,
1756 a3: 6,
1757 gap: 4,
1758 dir2: 1,
1759 dir3: -1,
1760 pulse: "both",
1761 dots: 8
1762 }
1763 });
1764 const CX = 61.26;
1765 const CY = 61.26;
1766 const DISC_R = 58.453;
1767 const W_PATHS = '<path d="m8.708 61.26c0 20.802 12.089 38.779 29.619 47.298l-25.069-68.686c-2.916 6.536-4.55 13.769-4.55 21.388z"/><path d="m96.74 58.608c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.501-34.493-8.188-22.434c-2.83-.166-5.511-.501-5.511-.501-2.832-.166-2.5-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.853.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989z"/><path d="m62.184 65.857-15.768 45.819c4.708 1.384 9.687 2.141 14.846 2.141 6.12 0 11.989-1.058 17.452-2.979-.141-.225-.269-.464-.374-.724z"/><path d="m107.376 36.046c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215z"/>';
1768 const _WpdSpinner = class _WpdSpinner extends Component {
1769 constructor() {
1770 super(...arguments);
1771 this._paintScheduled = false;
1772 }
1773 connectedCallback() {
1774 super.connectedCallback();
1775 this._schedulePaint();
1776 }
1777 render() {
1778 return html`<div class="root" part="root"></div>`;
1779 }
1780 requestUpdate() {
1781 super.requestUpdate();
1782 this._schedulePaint();
1783 }
1784 _schedulePaint() {
1785 if (this._paintScheduled || !this.isConnected) {
1786 return;
1787 }
1788 this._paintScheduled = true;
1789 queueMicrotask(() => {
1790 this._paintScheduled = false;
1791 if (!this.isConnected) {
1792 return;
1793 }
1794 this._paint();
1795 });
1796 }
1797 _paint() {
1798 this._syncCssVars();
1799 const root = this.shadowRoot?.querySelector(
1800 ".root"
1801 );
1802 if (!root) {
1803 return;
1804 }
1805 root.innerHTML = this._buildSvg();
1806 }
1807 /**
1808 * Reflect the color / accent / size attributes onto CSS custom
1809 * properties on the host. Removing the attribute clears the var
1810 * so the default cascades back in.
1811 */
1812 _syncCssVars() {
1813 const sync = (attr, varName, transform) => {
1814 const v = this.getAttribute(attr);
1815 if (v === null) {
1816 this.style.removeProperty(varName);
1817 } else {
1818 this.style.setProperty(
1819 varName,
1820 transform ? transform(v) : v
1821 );
1822 }
1823 };
1824 sync("color", "--wpd-spinner-color");
1825 sync("accent", "--wpd-spinner-accent");
1826 sync(
1827 "size",
1828 "--wpd-spinner-size",
1829 (v) => /^-?\d+(\.\d+)?$/.test(v.trim()) ? `${v}px` : v
1830 );
1831 }
1832 _effectiveConfig() {
1833 const presetName = this.getAttribute("preset") ?? "classic";
1834 const preset = WPD_SPINNER_PRESETS[presetName] ?? WPD_SPINNER_PRESETS.classic;
1835 const num = (attr, fallback) => {
1836 const v = this.getAttribute(attr);
1837 if (v === null) {
1838 return fallback;
1839 }
1840 const n = parseFloat(v);
1841 return Number.isFinite(n) ? n : fallback;
1842 };
1843 const dir = (attr, fallback) => {
1844 const v = this.getAttribute(attr);
1845 if (v === null) {
1846 return fallback;
1847 }
1848 const lc = v.toLowerCase();
1849 if (lc === "-1" || lc === "ccw" || lc === "reverse") {
1850 return -1;
1851 }
1852 return 1;
1853 };
1854 const pulse = () => {
1855 const v = this.getAttribute("pulse");
1856 if (v === "scale" || v === "opacity" || v === "both" || v === "none") {
1857 return v;
1858 }
1859 return preset.pulse;
1860 };
1861 return {
1862 sp1: num("sp1", preset.sp1),
1863 sp2: num("sp2", preset.sp2),
1864 sp3: num("sp3", preset.sp3),
1865 a1: num("a1", preset.a1),
1866 a2: num("a2", preset.a2),
1867 a3: num("a3", preset.a3),
1868 gap: num("gap", preset.gap),
1869 dir2: dir("dir2", preset.dir2),
1870 dir3: dir("dir3", preset.dir3),
1871 pulse: pulse(),
1872 dots: Math.max(0, Math.floor(num("dots", preset.dots)))
1873 };
1874 }
1875 _buildSvg() {
1876 const cfg = this._effectiveConfig();
1877 const label = escAttr(this.getAttribute("label") ?? "Loading");
1878 const pad = cfg.gap * 3 + 14;
1879 const vbMin = -pad;
1880 const vbSize = 122.52 + pad * 2;
1881 const r1 = DISC_R + cfg.gap + 2;
1882 const r2 = r1 + cfg.gap + 2;
1883 const r3 = r2 + cfg.gap + 1.5;
1884 const ring1Anim = `animation: wpd-spinner-spin ${(cfg.sp1 / 10).toFixed(2)}s linear infinite`;
1885 const ring2Anim = `animation: wpd-spinner-spin ${(cfg.sp2 / 10).toFixed(2)}s linear infinite${cfg.dir2 < 0 ? " reverse" : ""}`;
1886 const ring3Anim = `animation: wpd-spinner-spin ${(cfg.sp3 / 10).toFixed(2)}s linear infinite${cfg.dir3 < 0 ? " reverse" : ""}`;
1887 const pspd = (cfg.sp1 * 1.8 / 10).toFixed(1);
1888 const ospd = (cfg.sp1 * 2.3 / 10).toFixed(1);
1889 let pulseStyle = "";
1890 if (cfg.pulse === "scale") {
1891 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite`;
1892 } else if (cfg.pulse === "opacity") {
1893 pulseStyle = `animation: wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
1894 } else if (cfg.pulse === "both") {
1895 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite, wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
1896 }
1897 let dotEls = "";
1898 if (cfg.dots > 0) {
1899 const dr = r3 + cfg.gap + 1;
1900 const dc2 = 2 * Math.PI * dr;
1901 const dsz = 1.6;
1902 const dotDur = (cfg.sp1 * 0.65 / 10).toFixed(2);
1903 for (let i = 0; i < cfg.dots; i++) {
1904 const offset = -(i / cfg.dots) * dc2;
1905 dotEls += `<circle cx="${CX}" cy="${CY}" r="${dr.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="${dsz}" stroke-dasharray="${dsz.toFixed(2)} ${(dc2 - dsz).toFixed(2)}" stroke-dashoffset="${offset.toFixed(2)}" stroke-linecap="round" stroke-opacity="0.65" style="transform-origin:${CX}px ${CY}px;animation: wpd-spinner-spin ${dotDur}s linear infinite"/>`;
1906 }
1907 }
1908 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vbMin} ${vbMin} ${vbSize} ${vbSize}" role="img" aria-label="${label}"><g style="transform-origin:${CX}px ${CY}px${pulseStyle ? ";" + pulseStyle : ""}"><circle cx="${CX}" cy="${CY}" r="${DISC_R}" fill="currentColor"/><g class="mark">${W_PATHS}</g></g><circle cx="${CX}" cy="${CY}" r="${r1.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="0.6" stroke-opacity="0.2"/><circle cx="${CX}" cy="${CY}" r="${r1.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="2.2" stroke-dasharray="${dasharray(r1, cfg.a1)}" stroke-linecap="round" style="transform-origin:${CX}px ${CY}px;${ring1Anim}"/><circle cx="${CX}" cy="${CY}" r="${r2.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="0.5" stroke-opacity="0.15"/><circle cx="${CX}" cy="${CY}" r="${r2.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="1.6" stroke-opacity="0.8" stroke-dasharray="${dasharray(r2, cfg.a2)}" stroke-linecap="round" style="transform-origin:${CX}px ${CY}px;${ring2Anim}"/><circle cx="${CX}" cy="${CY}" r="${r3.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="0.4" stroke-opacity="0.12"/><circle cx="${CX}" cy="${CY}" r="${r3.toFixed(2)}" fill="none" stroke="currentColor" stroke-width="1.0" stroke-opacity="0.6" stroke-dasharray="${dasharray(r3, cfg.a3)}" stroke-linecap="round" style="transform-origin:${CX}px ${CY}px;${ring3Anim}"/>` + dotEls + `</svg>`;
1909 }
1910 };
1911 _WpdSpinner.props = [
1912 "preset",
1913 "size",
1914 "color",
1915 "accent",
1916 "sp1",
1917 "sp2",
1918 "sp3",
1919 "a1",
1920 "a2",
1921 "a3",
1922 "gap",
1923 "dir2",
1924 "dir3",
1925 "pulse",
1926 "dots",
1927 "label"
1928 ];
1929 _WpdSpinner.styles = [styles$1];
1930 _WpdSpinner.help = {
1931 title: "Spinner",
1932 summary: "Animated WordPress-mark loading indicator with four curated presets and full per-attribute overrides. CSS variables drive disc + accent colors and size; reduced-motion preferences are respected.",
1933 status: "experimental",
1934 since: "0.6.0",
1935 props: [
1936 {
1937 name: "preset",
1938 type: '"classic" | "comet" | "orbit" | "pulse"',
1939 default: "classic",
1940 description: "Visual personality. Every other attribute defaults to the preset's value and can be overridden individually."
1941 },
1942 {
1943 name: "size",
1944 type: "integer (px) or CSS length",
1945 default: "48",
1946 description: "Sets `--wpd-spinner-size`. Bare numbers are treated as px; pass a CSS length (e.g. `2em`) to opt into ems / rems."
1947 },
1948 {
1949 name: "color",
1950 type: "CSS color",
1951 description: "Disc + ring + dot color. Sets `--wpd-spinner-color`. Default inherits the WP admin theme color."
1952 },
1953 {
1954 name: "accent",
1955 type: "CSS color",
1956 default: "#fff",
1957 description: "Color of the W mark inside the disc. Sets `--wpd-spinner-accent`. Default white — change for dark-on-light or themed marks."
1958 },
1959 {
1960 name: "sp1, sp2, sp3",
1961 type: "integer (deciseconds)",
1962 description: "Per-ring rotation duration in tenths-of-a-second (12 → 1.2s). Higher = slower."
1963 },
1964 {
1965 name: "a1, a2, a3",
1966 type: "integer (0-100)",
1967 description: "Per-ring arc length as a percentage of the ring circumference."
1968 },
1969 {
1970 name: "gap",
1971 type: "integer",
1972 description: "Gap between concentric rings (units approximate to px at 120-viewport)."
1973 },
1974 {
1975 name: "dir2, dir3",
1976 type: '"1" | "-1" | "cw" | "ccw"',
1977 description: "Per-ring direction; ring 1 is always clockwise."
1978 },
1979 {
1980 name: "pulse",
1981 type: '"none" | "scale" | "opacity" | "both"',
1982 description: "Pulse animation applied to the disc + W mark."
1983 },
1984 {
1985 name: "dots",
1986 type: "integer",
1987 description: "Outer trailing dot count. Sensible values: 0, 3, 5, 8."
1988 },
1989 {
1990 name: "label",
1991 type: "string",
1992 default: "Loading",
1993 description: 'Accessible name for the SVG (`role="img"` + `aria-label`).'
1994 }
1995 ],
1996 cssProps: [
1997 { name: "--wpd-spinner-color", default: "var(--wp-admin-theme-color, #21759b)" },
1998 { name: "--wpd-spinner-accent", default: "#fff" },
1999 { name: "--wpd-spinner-size", default: "48px" }
2000 ],
2001 example: html`<wpd-spinner preset="comet" size="80"></wpd-spinner>`
2002 };
2003 let WpdSpinner = _WpdSpinner;
2004 function dasharray(r, pct) {
2005 const c = 2 * Math.PI * r;
2006 const visible = pct / 100 * c;
2007 const gap = c - visible;
2008 return `${visible.toFixed(2)} ${gap.toFixed(2)}`;
2009 }
2010 function escAttr(s) {
2011 return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2012 }
2013 defineComponent("wpd-spinner", WpdSpinner);
2014 const styles = css`:host{display:inline-flex}:host( [ fill-cell ] ){display:flex;width:100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:var( --wpd-button-padding,6px 12px );border-radius:var( --wpd-button-border-radius,6px );font:inherit;font-weight:500;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease;background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid var( --desktop-mode-border,#c3c4c7 ) )}:host( [ fill-cell ] ) button{width:100%;min-height:var( --wpd-button-min-height,44px )}button:disabled{opacity:0.5;cursor:not-allowed}button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.04 ) )}:host( [ variant='primary' ] ) button{background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) );color:var( --wpd-button-fg,#fff );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='primary' ] ) button:hover:not(:disabled ){filter:brightness( 1.06 );background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ variant='secondary' ] ) button{background:var( --wpd-button-bg,rgba( 0,0,0,0.06 ) );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='secondary' ] ) button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.1 ) )}:host( [ variant='danger' ] ) button{background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,#d63638 );border:var( --wpd-button-border,1px solid currentColor )}:host( [ variant='danger' ] ) button:hover:not(:disabled ){background:#d63638;color:#fff}:host( [ variant='link' ] ) button{background:transparent;color:var( --wpd-button-fg,var( --wp-admin-theme-color,#2271b1 ) );border:0;padding:0;text-decoration:underline}:host( [ busy ] ) button{pointer-events:none;opacity:0.75}.wpd-button__spinner{box-sizing:border-box;display:inline-block;width:12px;height:12px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:wpd-button-spin 0.6s linear infinite;flex-shrink:0}@keyframes wpd-button-spin{to{transform:rotate( 360deg )}}`;
2015 const _WpdButton = class _WpdButton extends Component {
2016 render() {
2017 const disabled = this.disabled !== null;
2018 const busy = this.busy !== null;
2019 const type = this.type || "button";
2020 return html`
2021 <button
2022 part="button"
2023 type=${type}
2024 ?disabled=${disabled || busy}
2025 aria-busy=${busy ? "true" : "false"}
2026 >
2027 ${busy ? html`<span class="wpd-button__spinner" aria-hidden="true"></span>` : ""}
2028 <slot></slot>
2029 </button>
2030 `;
2031 }
2032 };
2033 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
2034 _WpdButton.styles = [styles];
2035 _WpdButton.help = {
2036 title: "Button",
2037 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
2038 status: "stable",
2039 since: "0.9.0",
2040 props: [
2041 {
2042 name: "variant",
2043 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
2044 default: "ghost",
2045 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
2046 },
2047 {
2048 name: "disabled",
2049 type: "boolean attribute",
2050 description: "Disable pointer + keyboard interaction and dim the chrome."
2051 },
2052 {
2053 name: "type",
2054 type: "'button' | 'submit' | 'reset'",
2055 default: "button",
2056 description: "Forwarded to the underlying native <button>."
2057 },
2058 {
2059 name: "busy",
2060 type: "boolean attribute",
2061 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
2062 },
2063 {
2064 name: "fill-cell",
2065 type: "boolean attribute",
2066 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
2067 }
2068 ],
2069 slots: [{ name: "(default)", description: "Button label." }],
2070 parts: [{ name: "button", description: "Underlying <button> element." }],
2071 cssProps: [
2072 { name: "--wpd-button-bg", description: "Background color." },
2073 {
2074 name: "--wpd-button-bg-hover",
2075 description: "Hover wash (ghost + secondary variants)."
2076 },
2077 { name: "--wpd-button-fg", description: "Text color." },
2078 { name: "--wpd-button-border", description: "Border shorthand." },
2079 { name: "--wpd-button-border-radius", default: "6px" },
2080 { name: "--wpd-button-padding", default: "6px 12px" },
2081 {
2082 name: "--wpd-button-min-height",
2083 description: "Minimum height when fill-cell is set."
2084 }
2085 ],
2086 example: html`
2087 <wpd-cluster gap="8">
2088 <wpd-button variant="primary">Primary</wpd-button>
2089 <wpd-button variant="secondary">Secondary</wpd-button>
2090 <wpd-button variant="ghost">Ghost</wpd-button>
2091 <wpd-button variant="danger">Danger</wpd-button>
2092 <wpd-button variant="link">Link</wpd-button>
2093 </wpd-cluster>
2094 `
2095 };
2096 let WpdButton = _WpdButton;
2097 defineComponent("wpd-button", WpdButton);
2098 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}`;
2099 const _WpdTextField = class _WpdTextField extends Component {
2100 constructor() {
2101 super(...arguments);
2102 this._revealed = false;
2103 }
2104 connectedCallback() {
2105 super.connectedCallback();
2106 ensureAutoId(this);
2107 }
2108 render() {
2109 const label = this.label || "";
2110 const value = this.value ?? "";
2111 const placeholder = this.placeholder || "";
2112 const disabled = this.disabled !== null;
2113 const readonly = this.readonly !== null;
2114 const declaredAutocomplete = this.autocomplete;
2115 const declaredType = this.type || "text";
2116 const isPassword = declaredType === "password";
2117 let autocomplete = declaredAutocomplete || "off";
2118 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
2119 autocomplete = "new-password";
2120 }
2121 const maxLength = this.maxlength;
2122 const minLength = this.minlength;
2123 const pattern = this.pattern || "";
2124 const name = this.name || "";
2125 const suffix = this.suffix || "";
2126 const invalid = this.invalid !== null;
2127 const reveal = this.reveal !== null;
2128 const isPasswordIntent = declaredType === "password";
2129 const isMasked = isPasswordIntent && !(reveal && this._revealed);
2130 let effectiveType;
2131 if (isPasswordIntent) {
2132 effectiveType = "text";
2133 } else if (reveal && this._revealed) {
2134 effectiveType = "text";
2135 } else {
2136 effectiveType = declaredType;
2137 }
2138 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
2139 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
2140 const hostId = this.id || "wpd-unnamed";
2141 const inputId = `${hostId}__input`;
2142 return html`
2143 ${label ? html`<label
2144 class="wpd-text-field__label"
2145 for=${inputId}
2146 >${label}</label>` : html``}
2147 <span class=${rowClass}>
2148 <input
2149 id=${inputId}
2150 class=${inputClass}
2151 type=${effectiveType}
2152 .value=${value}
2153 placeholder=${placeholder}
2154 ?disabled=${disabled}
2155 ?readonly=${readonly}
2156 autocomplete=${autocomplete}
2157 maxlength=${maxLength ?? ""}
2158 minlength=${minLength ?? ""}
2159 pattern=${pattern}
2160 name=${name}
2161 aria-invalid=${invalid ? "true" : "false"}
2162 aria-label=${label || ""}
2163 @input=${(e) => this._onInput(e)}
2164 @change=${(e) => this._onChange(e)}
2165 @keydown=${(e) => this._onKeyDown(e)}
2166 />
2167 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
2168 ${reveal ? this._renderRevealButton(disabled) : html``}
2169 </span>
2170 `;
2171 }
2172 _renderRevealButton(disabled) {
2173 const label = this._revealed ? "Hide" : "Show";
2174 return html`
2175 <button
2176 type="button"
2177 class="wpd-text-field__reveal"
2178 aria-label=${label}
2179 aria-pressed=${this._revealed ? "true" : "false"}
2180 ?disabled=${disabled}
2181 tabindex="0"
2182 @click=${() => this._onToggleReveal()}
2183 >
2184 ${this._revealed ? _iconEyeOff() : _iconEye()}
2185 </button>
2186 `;
2187 }
2188 _onToggleReveal() {
2189 this._revealed = !this._revealed;
2190 this.requestUpdate();
2191 }
2192 _onInput(e) {
2193 const input = e.target;
2194 this.value = input.value;
2195 this.emit("wpd-input-change", { value: input.value });
2196 }
2197 _onChange(e) {
2198 const input = e.target;
2199 this.emit("wpd-input-commit", { value: input.value });
2200 }
2201 _onKeyDown(e) {
2202 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
2203 const input = e.target;
2204 this.emit("wpd-submit", { value: input.value });
2205 }
2206 }
2207 };
2208 _WpdTextField.props = [
2209 "label",
2210 "value",
2211 "placeholder",
2212 "disabled",
2213 "readonly",
2214 "autocomplete",
2215 "type",
2216 "maxlength",
2217 "minlength",
2218 "pattern",
2219 "name",
2220 "suffix",
2221 "invalid",
2222 "reveal"
2223 ];
2224 _WpdTextField.styles = [textFieldStyles];
2225 _WpdTextField.help = {
2226 title: "Text field",
2227 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.",
2228 status: "stable",
2229 since: "0.5.0",
2230 props: [
2231 { name: "label", type: "string", description: "Visible label above the input." },
2232 { name: "value", type: "string", description: "Current input value; reflected two-way." },
2233 { name: "placeholder", type: "string", description: "Native placeholder string." },
2234 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
2235 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
2236 {
2237 name: "autocomplete",
2238 type: "string",
2239 default: "off",
2240 description: "Forwarded to the native input autocomplete attribute."
2241 },
2242 {
2243 name: "type",
2244 type: "string",
2245 default: "text",
2246 description: "Native input type (text, password, email, search, tel, url)."
2247 },
2248 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
2249 { name: "minlength", type: "integer (string)", description: "Native minlength." },
2250 { name: "pattern", type: "regex string", description: "Native validation pattern." },
2251 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
2252 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
2253 {
2254 name: "invalid",
2255 type: "boolean attribute",
2256 description: "Marks the field aria-invalid and applies the error style."
2257 },
2258 {
2259 name: "reveal",
2260 type: "boolean attribute",
2261 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
2262 }
2263 ],
2264 events: [
2265 {
2266 name: "wpd-input-change",
2267 description: "Fires on every input keystroke.",
2268 detail: "{ value: string }"
2269 },
2270 {
2271 name: "wpd-input-commit",
2272 description: "Fires on the native change event (blur / Enter).",
2273 detail: "{ value: string }"
2274 },
2275 {
2276 name: "wpd-submit",
2277 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
2278 detail: "{ value: string }"
2279 }
2280 ],
2281 cssProps: [
2282 { name: "--desktop-mode-text", description: "Text colour." },
2283 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
2284 { name: "--desktop-mode-border", description: "Input outline." },
2285 { name: "--desktop-mode-window-bg", description: "Input background." }
2286 ],
2287 example: html`
2288 <wpd-stack gap="8">
2289 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
2290 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
2291 </wpd-stack>
2292 `
2293 };
2294 let WpdTextField = _WpdTextField;
2295 defineComponent("wpd-text-field", WpdTextField);
2296 function _iconEye() {
2297 return html`
2298 <svg
2299 viewBox="0 0 16 16"
2300 width="14"
2301 height="14"
2302 fill="none"
2303 stroke="currentColor"
2304 stroke-width="1.5"
2305 stroke-linecap="round"
2306 stroke-linejoin="round"
2307 aria-hidden="true"
2308 focusable="false"
2309 >
2310 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
2311 <circle cx="8" cy="8" r="2" />
2312 </svg>
2313 `;
2314 }
2315 function _iconEyeOff() {
2316 return html`
2317 <svg
2318 viewBox="0 0 16 16"
2319 width="14"
2320 height="14"
2321 fill="none"
2322 stroke="currentColor"
2323 stroke-width="1.5"
2324 stroke-linecap="round"
2325 stroke-linejoin="round"
2326 aria-hidden="true"
2327 focusable="false"
2328 >
2329 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
2330 <circle cx="8" cy="8" r="2" />
2331 <line x1="2" y1="2" x2="14" y2="14" />
2332 </svg>
2333 `;
2334 }
2335 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 )}`;
2336 const optionStyles = css`:host{display:none}`;
2337 const _WpdOption = class _WpdOption extends Component {
2338 render() {
2339 return html``;
2340 }
2341 };
2342 _WpdOption.props = ["value", "disabled"];
2343 _WpdOption.styles = [optionStyles];
2344 _WpdOption.help = {
2345 title: "Option",
2346 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>.",
2347 status: "stable",
2348 since: "0.5.0",
2349 props: [
2350 {
2351 name: "value",
2352 type: "string",
2353 description: "Option identifier read by the parent <wpd-select>."
2354 },
2355 {
2356 name: "disabled",
2357 type: "boolean attribute",
2358 description: "Renders the option disabled in the parent <select>."
2359 }
2360 ],
2361 slots: [
2362 { name: "(default)", description: "Label text read from textContent." }
2363 ]
2364 };
2365 let WpdOption = _WpdOption;
2366 defineComponent("wpd-option", WpdOption);
2367 const _WpdSelect = class _WpdSelect extends Component {
2368 constructor() {
2369 super(...arguments);
2370 this._optionObserver = null;
2371 }
2372 /**
2373 * Declarative item-list setter. Replaces the existing
2374 * `<wpd-option>` children with a fresh set; preserves `value`
2375 * when it still matches, otherwise clears to the placeholder.
2376 *
2377 * Same shape as the setter on `<wpd-segmented>` so callers can
2378 * swap tag names (segmented ↔ select) without touching the
2379 * populate code when an option list outgrows the pill bar.
2380 *
2381 * ```js
2382 * select.items = [
2383 * { value: 'eur', label: 'Euro' },
2384 * { value: 'usd', label: 'US Dollar' },
2385 * ];
2386 * ```
2387 *
2388 * @since 0.5.0
2389 */
2390 set items(list) {
2391 const existing = this.querySelectorAll(":scope > wpd-option");
2392 for (const el of Array.from(existing)) {
2393 el.remove();
2394 }
2395 for (const item of list) {
2396 const opt = document.createElement("wpd-option");
2397 opt.setAttribute("value", item.value);
2398 opt.textContent = item.label;
2399 this.appendChild(opt);
2400 }
2401 const current = this.value;
2402 const stillValid = current !== null && list.some((i) => i.value === current);
2403 if (!stillValid && list.length > 0) {
2404 this.value = list[0].value;
2405 }
2406 this.requestUpdate();
2407 }
2408 connectedCallback() {
2409 super.connectedCallback();
2410 ensureAutoId(this);
2411 this._optionObserver = new MutationObserver(() => this.requestUpdate());
2412 this._optionObserver.observe(this, {
2413 childList: true,
2414 subtree: true,
2415 attributes: true,
2416 attributeFilter: ["value", "disabled"],
2417 characterData: true
2418 });
2419 }
2420 disconnectedCallback() {
2421 this._optionObserver?.disconnect();
2422 this._optionObserver = null;
2423 }
2424 render() {
2425 const label = this.label || "";
2426 const current = this.value;
2427 const placeholder = this.placeholder || "";
2428 const disabled = this.disabled !== null;
2429 const name = this.name || "";
2430 if (label) {
2431 this.setAttribute("aria-label", label);
2432 } else {
2433 this.removeAttribute("aria-label");
2434 }
2435 const selectAriaLabel = label || placeholder;
2436 const options = this._readOptions();
2437 const hostId = this.id || "wpd-unnamed";
2438 const selectId = `${hostId}__input`;
2439 return html`
2440 ${label ? html`<label
2441 class="wpd-select__label"
2442 for=${selectId}
2443 >${label}</label>` : html``}
2444 <span class="wpd-select__wrap">
2445 <select
2446 id=${selectId}
2447 ?disabled=${disabled}
2448 aria-label=${selectAriaLabel}
2449 name=${name}
2450 @change=${(e) => this._onChange(e)}
2451 >
2452 ${placeholder && !current ? html`<option value="" disabled selected>
2453 ${placeholder}
2454 </option>` : html``}
2455 ${options.map(
2456 (o) => html`
2457 <option
2458 value=${o.value}
2459 ?disabled=${o.disabled}
2460 ?selected=${o.value === current}
2461 >
2462 ${o.label}
2463 </option>
2464 `
2465 )}
2466 </select>
2467 <!--
2468 Inline SVG — the previous dashicons-classed span
2469 never painted because the global Dashicons font
2470 stylesheet cannot cross the shadow-root boundary.
2471 An inline SVG lives inside the shadow tree, inherits
2472 currentColor via the stroke attribute, and needs
2473 no external CSS.
2474 -->
2475 <svg
2476 class="wpd-select__chevron"
2477 viewBox="0 0 12 12"
2478 width="12"
2479 height="12"
2480 aria-hidden="true"
2481 focusable="false"
2482 >
2483 <path
2484 d="M3 5l3 3 3-3"
2485 stroke="currentColor"
2486 stroke-width="1.4"
2487 stroke-linecap="round"
2488 stroke-linejoin="round"
2489 fill="none"
2490 ></path>
2491 </svg>
2492 </span>
2493 `;
2494 }
2495 _readOptions() {
2496 const out = [];
2497 const children = this.querySelectorAll(":scope > wpd-option");
2498 for (const child of Array.from(children)) {
2499 const value = child.getAttribute("value");
2500 if (value === null) {
2501 continue;
2502 }
2503 out.push({
2504 value,
2505 label: (child.textContent || value).trim(),
2506 disabled: child.hasAttribute("disabled")
2507 });
2508 }
2509 return out;
2510 }
2511 _onChange(e) {
2512 const sel = e.target;
2513 const next = sel.value;
2514 this.value = next;
2515 this.emit("wpd-pick", { value: next });
2516 }
2517 };
2518 _WpdSelect.props = [
2519 "value",
2520 "label",
2521 "placeholder",
2522 "disabled",
2523 "name"
2524 ];
2525 _WpdSelect.styles = [selectStyles];
2526 _WpdSelect.help = {
2527 title: "Select",
2528 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.",
2529 status: "stable",
2530 since: "0.5.0",
2531 props: [
2532 {
2533 name: "value",
2534 type: "string",
2535 description: "Currently selected option value."
2536 },
2537 {
2538 name: "label",
2539 type: "string",
2540 description: "Visible label rendered above the select and forwarded to the native control as aria-label."
2541 },
2542 {
2543 name: "placeholder",
2544 type: "string",
2545 description: "Disabled leading option shown when no value is set."
2546 },
2547 {
2548 name: "disabled",
2549 type: "boolean attribute",
2550 description: "Disables the native select and dims the chrome."
2551 },
2552 {
2553 name: "name",
2554 type: "string",
2555 description: "Forwarded to the native <select name=…> for form submission."
2556 }
2557 ],
2558 slots: [
2559 { name: "(default)", description: '<wpd-option value="…"> children.' }
2560 ],
2561 events: [
2562 {
2563 name: "wpd-pick",
2564 description: "Fires when the user picks a new option.",
2565 detail: "{ value: string }"
2566 }
2567 ],
2568 cssProps: [
2569 { name: "--desktop-mode-text", description: "Label + value colour." },
2570 { name: "--desktop-mode-muted", description: "Placeholder + chevron colour." }
2571 ],
2572 example: html`
2573 <wpd-select value="eur" label="Currency">
2574 <wpd-option value="eur">Euro</wpd-option>
2575 <wpd-option value="usd">US Dollar</wpd-option>
2576 <wpd-option value="jpy">Japanese Yen</wpd-option>
2577 </wpd-select>
2578 `
2579 };
2580 let WpdSelect = _WpdSelect;
2581 defineComponent("wpd-select", WpdSelect);
2582 })();
2583