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

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

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