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

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

2,567 lines 102.1 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.5.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 TEXT_DOMAIN = "desktop-mode";
670 function i18n() {
671 return window.wp?.i18n;
672 }
673 function __(text, domain = TEXT_DOMAIN) {
674 return i18n()?.__(text, domain) ?? text;
675 }
676 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}`;
677 const toastStyles = css`:host{display:flex;align-items:center;gap:12px;min-width:280px;max-width:420px;padding:10px 14px;background:#1d2327;color:#fff;border-radius:10px;border:1px solid rgba( 255,255,255,0.12 );box-shadow:0 10px 30px rgba( 0,0,0,0.4 ),0 2px 6px rgba( 0,0,0,0.18 ),inset 0 0 0 1px rgba( 255,255,255,0.04 );font-size:13px;line-height:1.4;opacity:0;transform:translateY( -8px );transition:opacity 0.18s ease,transform 0.18s ease;pointer-events:auto}:host( [ state='in' ] ){opacity:1;transform:translateY( 0 )}:host( [ state='out' ] ){opacity:0;transform:translateY( -8px )}.wpd-toast__label{flex:1}button{flex-shrink:0;padding:4px 10px;border:none;border-radius:4px;background:rgba( 255,255,255,0.12 );color:#fff;font:inherit;font-size:12px;font-weight:500;cursor:pointer;transition:background-color 0.12s ease}button:hover{background:rgba( 255,255,255,0.22 )}button:focus-visible{outline:2px solid rgba( 255,255,255,0.6 );outline-offset:2px}.wpd-toast__close{display:inline-flex;align-items:center;justify-content:center;padding:4px;border-radius:6px;background:transparent;color:rgba( 255,255,255,0.7 )}.wpd-toast__close:hover{background:rgba( 255,255,255,0.14 );color:#fff}@media ( prefers-reduced-motion:reduce ){:host{transition-duration:0.01ms}}`;
678 const _WpdToastContainer = class _WpdToastContainer extends Component {
679 connectedCallback() {
680 super.connectedCallback();
681 this.setAttribute("aria-live", "polite");
682 }
683 render() {
684 return html`<slot></slot>`;
685 }
686 };
687 _WpdToastContainer.styles = [containerStyles];
688 _WpdToastContainer.help = {
689 title: "Toast container",
690 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
691 status: "stable",
692 since: "0.9.0",
693 slots: [
694 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
695 ],
696 cssProps: [
697 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
698 ],
699 example: html`
700 <wpd-toast-container>
701 <wpd-toast state="in">Settings saved.</wpd-toast>
702 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
703 </wpd-toast-container>
704 `
705 };
706 let WpdToastContainer = _WpdToastContainer;
707 defineComponent("wpd-toast-container", WpdToastContainer);
708 const _WpdToast = class _WpdToast extends Component {
709 connectedCallback() {
710 super.connectedCallback();
711 if (!this.hasAttribute("role")) {
712 this.setAttribute("role", "status");
713 }
714 }
715 render() {
716 const action = this.action || "";
717 const dismissible = this.hasAttribute("dismissible");
718 return html`
719 <span class="wpd-toast__label"><slot></slot></span>
720 <button
721 type="button"
722 ?hidden=${!action}
723 @click=${(e) => this._onAction(e)}
724 >
725 ${action}
726 </button>
727 <button
728 type="button"
729 class="wpd-toast__close"
730 aria-label=${__("Dismiss")}
731 ?hidden=${!dismissible}
732 @click=${(e) => this._onDismiss(e)}
733 >
734 <svg viewBox="0 0 14 14" width="12" height="12" aria-hidden="true" focusable="false">
735 <path
736 d="M3 3 L11 11 M11 3 L3 11"
737 stroke="currentColor"
738 stroke-width="1.7"
739 stroke-linecap="round"
740 fill="none"
741 ></path>
742 </svg>
743 </button>
744 `;
745 }
746 _onAction(e) {
747 e.preventDefault();
748 e.stopPropagation();
749 this.emit("wpd-toast-action", {});
750 }
751 _onDismiss(e) {
752 e.preventDefault();
753 e.stopPropagation();
754 this.emit("wpd-toast-dismiss", {});
755 }
756 };
757 _WpdToast.props = ["action", "state", "dismissible"];
758 _WpdToast.styles = [toastStyles];
759 _WpdToast.help = {
760 title: "Toast",
761 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.',
762 status: "stable",
763 since: "0.9.0",
764 props: [
765 {
766 name: "action",
767 type: "string",
768 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
769 },
770 {
771 name: "state",
772 type: "'in' | 'out'",
773 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
774 },
775 {
776 name: "dismissible",
777 type: "boolean",
778 description: "When set, a close (×) button renders on the right and emits wpd-toast-dismiss on click. Use for persistent toasts the user must be able to close."
779 }
780 ],
781 slots: [
782 { name: "(default)", description: "Message text." }
783 ],
784 events: [
785 {
786 name: "wpd-toast-action",
787 description: "Fires when the action button is clicked.",
788 detail: "{}"
789 },
790 {
791 name: "wpd-toast-dismiss",
792 description: "Fires when the close (×) button is clicked.",
793 detail: "{}"
794 }
795 ],
796 example: html`
797 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
798 `
799 };
800 let WpdToast = _WpdToast;
801 defineComponent("wpd-toast", WpdToast);
802 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 )}`;
803 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
804 constructor() {
805 super(...arguments);
806 this._onKey = (e) => {
807 if (e.key === "Escape") {
808 e.preventDefault();
809 this._cancel();
810 }
811 if (e.key === "Enter" && !e.isComposing) {
812 e.preventDefault();
813 this._confirm();
814 }
815 };
816 this._onBackdrop = (e) => {
817 const path = e.composedPath();
818 const original = path.length > 0 ? path[0] : e.target;
819 if (original === this) {
820 this._cancel();
821 }
822 };
823 this._confirm = () => {
824 this.emit("wpd-confirm", { confirmed: true });
825 this.removeAttribute("open");
826 };
827 this._cancel = () => {
828 this.emit("wpd-cancel", { confirmed: false });
829 this.removeAttribute("open");
830 };
831 }
832 connectedCallback() {
833 super.connectedCallback();
834 this.setAttribute("role", "dialog");
835 this.setAttribute("aria-modal", "true");
836 this.addEventListener("keydown", this._onKey);
837 this.addEventListener("click", this._onBackdrop);
838 }
839 disconnectedCallback() {
840 this.removeEventListener("keydown", this._onKey);
841 this.removeEventListener("click", this._onBackdrop);
842 }
843 render() {
844 const title = this.title ?? "";
845 const message = this.message ?? "";
846 const confirmLabel = this["confirm-label"] || "Confirm";
847 const cancelLabel = this["cancel-label"] || "Cancel";
848 const isDanger = this.hasAttribute("danger");
849 const hideCancel = this.hasAttribute("hide-cancel");
850 const isDismissable = this.hasAttribute("dismissable");
851 return html`
852 <div class="dialog" tabindex="-1">
853 ${isDismissable ? html`<button
854 type="button"
855 class="close"
856 aria-label="Close"
857 @click=${() => this._cancel()}
858 >&times;</button>` : html``}
859 ${title ? html`<h2 class="title">${title}</h2>` : html``}
860 ${message ? html`<p class="message">${message}</p>` : html``}
861 <div class="actions">
862 ${hideCancel ? html`` : html`<button
863 type="button"
864 class="btn btn--secondary"
865 @click=${() => this._cancel()}
866 >
867 ${cancelLabel}
868 </button>`}
869 <button
870 type="button"
871 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
872 @click=${() => this._confirm()}
873 >
874 ${confirmLabel}
875 </button>
876 </div>
877 </div>
878 `;
879 }
880 };
881 _WpdConfirmDialog.props = [
882 "open",
883 "title",
884 "message",
885 "confirm-label",
886 "cancel-label",
887 "danger",
888 "hide-cancel",
889 "dismissable"
890 ];
891 _WpdConfirmDialog.styles = [dialogStyles];
892 _WpdConfirmDialog.help = {
893 title: "Confirm dialog",
894 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.",
895 status: "experimental",
896 since: "0.9.0",
897 props: [
898 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
899 { name: "title", type: "string", description: "Heading shown at the top." },
900 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
901 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
902 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
903 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
904 { 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." },
905 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
906 ],
907 events: [
908 {
909 name: "wpd-confirm",
910 description: "Fires on confirm. Detail: `{ confirmed: true }`."
911 },
912 {
913 name: "wpd-cancel",
914 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
915 }
916 ]
917 };
918 let WpdConfirmDialog = _WpdConfirmDialog;
919 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
920 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}`;
921 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}`;
922 const _WpdContextMenu = class _WpdContextMenu extends Component {
923 render() {
924 return html`
925 <slot></slot>
926 `;
927 }
928 connectedCallback() {
929 super.connectedCallback();
930 this.setAttribute("role", "menu");
931 }
932 };
933 _WpdContextMenu.props = ["open"];
934 _WpdContextMenu.styles = [menuStyles$1];
935 _WpdContextMenu.help = {
936 title: "Context menu",
937 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.",
938 status: "experimental",
939 since: "0.9.0",
940 props: [
941 {
942 name: "open",
943 type: "boolean attribute",
944 description: "Mounts the menu in its open / visible state."
945 }
946 ],
947 slots: [
948 { name: "(default)", description: "List of <wpd-context-menu-option> items." }
949 ],
950 events: [
951 {
952 name: "wpd-context-menu-pick",
953 description: "Bubbled from a non-disabled, non-heading option on activation. Detail: `{ id, value }`."
954 }
955 ]
956 };
957 let WpdContextMenu = _WpdContextMenu;
958 defineComponent("wpd-context-menu", WpdContextMenu);
959 const _WpdContextMenuOption = class _WpdContextMenuOption extends Component {
960 constructor() {
961 super(...arguments);
962 this._onActivate = (e) => {
963 if (this.hasAttribute("disabled") || this.hasAttribute("heading")) {
964 return;
965 }
966 const target = e.target;
967 if (target && target !== this && target.closest("wpd-context-menu-option") !== this) {
968 return;
969 }
970 this.emit("wpd-context-menu-pick", {
971 id: this.dataset.menuItemId ?? this.id ?? "",
972 value: this.getAttribute("value") ?? ""
973 });
974 };
975 this._onKey = (e) => {
976 if (e.key === "Enter" || e.key === " ") {
977 e.preventDefault();
978 this._onActivate(e);
979 }
980 };
981 }
982 connectedCallback() {
983 super.connectedCallback();
984 const isHeading = this.hasAttribute("heading");
985 this.setAttribute("role", isHeading ? "presentation" : "menuitem");
986 if (!isHeading) {
987 this.setAttribute("tabindex", "0");
988 }
989 this.addEventListener("click", this._onActivate);
990 this.addEventListener("keydown", this._onKey);
991 }
992 disconnectedCallback() {
993 this.removeEventListener("click", this._onActivate);
994 this.removeEventListener("keydown", this._onKey);
995 }
996 render() {
997 const icon = this.getAttribute("icon");
998 const hasChildren = this.hasAttribute("has-children");
999 const checked = this.hasAttribute("checked");
1000 return html`
1001 ${checked ? html`<span class="check" aria-hidden="true">✓</span>` : html``}
1002 ${icon ? html`<span class="icon dashicons ${icon}" aria-hidden="true"></span>` : html``}
1003 <span class="label"><slot></slot></span>
1004 ${hasChildren ? html`<span class="chevron" aria-hidden="true">›</span>` : html``}
1005 `;
1006 }
1007 };
1008 _WpdContextMenuOption.props = [
1009 "value",
1010 "icon",
1011 "disabled",
1012 "danger",
1013 "heading",
1014 "has-children",
1015 "checked"
1016 ];
1017 _WpdContextMenuOption.styles = [optionStyles$1];
1018 _WpdContextMenuOption.help = {
1019 title: "Context menu option",
1020 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.",
1021 status: "experimental",
1022 since: "0.9.0",
1023 props: [
1024 {
1025 name: "value",
1026 type: "string",
1027 description: "Forwarded as `detail.value` on activation."
1028 },
1029 {
1030 name: "icon",
1031 type: "string",
1032 description: "Dashicon class (e.g. `dashicons-trash`)."
1033 },
1034 {
1035 name: "disabled",
1036 type: "boolean attribute",
1037 description: "Renders the option dimmed; clicks are ignored."
1038 },
1039 {
1040 name: "danger",
1041 type: "boolean attribute",
1042 description: "Destructive styling — red text, red hover."
1043 },
1044 {
1045 name: "heading",
1046 type: "boolean attribute",
1047 description: "Non-interactive section header. Ignores clicks."
1048 },
1049 {
1050 name: "has-children",
1051 type: "boolean attribute",
1052 description: "Renders a trailing chevron to suggest a submenu."
1053 },
1054 {
1055 name: "checked",
1056 type: "boolean attribute",
1057 description: "Renders a leading check mark — for radio-style picks inside a submenu (e.g. the active Sort By order)."
1058 }
1059 ],
1060 slots: [
1061 { name: "(default)", description: "Visible label." }
1062 ],
1063 events: [
1064 {
1065 name: "wpd-context-menu-pick",
1066 description: "Bubbled on click / Enter for non-heading non-disabled options. Detail: `{ id, value }`."
1067 }
1068 ]
1069 };
1070 let WpdContextMenuOption = _WpdContextMenuOption;
1071 defineComponent("wpd-context-menu-option", WpdContextMenuOption);
1072 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}`;
1073 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 )}`;
1074 const _WpdMenu = class _WpdMenu extends Component {
1075 connectedCallback() {
1076 super.connectedCallback();
1077 this.setAttribute("role", "menu");
1078 }
1079 render() {
1080 return html`<slot></slot>`;
1081 }
1082 };
1083 _WpdMenu.styles = [menuStyles];
1084 _WpdMenu.help = {
1085 title: "Menu",
1086 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.",
1087 status: "stable",
1088 since: "0.9.0",
1089 slots: [
1090 { name: "(default)", description: "<wpd-menu-item> children." }
1091 ],
1092 cssProps: [
1093 { name: "--desktop-mode-window-bg", description: "Menu background." },
1094 { name: "--desktop-mode-window-border", description: "Menu border." },
1095 { name: "--desktop-mode-text", description: "Item text colour." }
1096 ],
1097 example: html`
1098 <wpd-menu>
1099 <wpd-menu-item value="new" icon="dashicons-plus">Open another window</wpd-menu-item>
1100 <wpd-menu-item value="startup" role="menuitemcheckbox" checked>Open on startup</wpd-menu-item>
1101 <wpd-menu-item value="close">Close window</wpd-menu-item>
1102 </wpd-menu>
1103 `
1104 };
1105 let WpdMenu = _WpdMenu;
1106 defineComponent("wpd-menu", WpdMenu);
1107 const _WpdMenuItem = class _WpdMenuItem extends Component {
1108 connectedCallback() {
1109 super.connectedCallback();
1110 if (!this.hasAttribute("role")) {
1111 this.setAttribute("role", "menuitem");
1112 }
1113 }
1114 render() {
1115 const icon = this.icon || "";
1116 const isCheckbox = this.getAttribute("role") === "menuitemcheckbox";
1117 const checked = this.checked !== null;
1118 if (isCheckbox) {
1119 this.setAttribute("aria-checked", checked ? "true" : "false");
1120 }
1121 return html`
1122 <button type="button" @click=${(e) => this._onPick(e)}>
1123 <span
1124 class="wpd-menu-item__check"
1125 ?hidden=${!isCheckbox}
1126 ></span>
1127 <span
1128 class="wpd-menu-item__icon dashicons ${icon}"
1129 aria-hidden="true"
1130 ?hidden=${isCheckbox || !icon}
1131 ></span>
1132 <span class="wpd-menu-item__label">
1133 <slot></slot>
1134 </span>
1135 </button>
1136 `;
1137 }
1138 _onPick(e) {
1139 e.preventDefault();
1140 this.emit("wpd-menu-item-click", {
1141 value: this.value
1142 });
1143 }
1144 };
1145 _WpdMenuItem.props = ["icon", "value", "checked"];
1146 _WpdMenuItem.styles = [menuItemStyles];
1147 _WpdMenuItem.help = {
1148 title: "Menu item",
1149 summary: 'Single row inside a <wpd-menu>. Supports three looks: plain label, left-aligned dashicon (icon="dashicons-…"), or a checkbox indicator (role="menuitemcheckbox" + checked).',
1150 status: "stable",
1151 since: "0.9.0",
1152 props: [
1153 {
1154 name: "icon",
1155 type: "string (dashicons class)",
1156 description: 'Dashicons class rendered on the left. Ignored when role="menuitemcheckbox".'
1157 },
1158 {
1159 name: "value",
1160 type: "string",
1161 description: "Identifier emitted in wpd-menu-item-click.detail.value."
1162 },
1163 {
1164 name: "checked",
1165 type: "boolean attribute",
1166 description: 'Visible check indicator. Only honoured when role="menuitemcheckbox".'
1167 }
1168 ],
1169 slots: [
1170 { name: "(default)", description: "Menu item label." }
1171 ],
1172 events: [
1173 {
1174 name: "wpd-menu-item-click",
1175 description: "Fires when the item is clicked; bubbles so the <wpd-menu> parent can delegate.",
1176 detail: "{ value: string | null }"
1177 }
1178 ]
1179 };
1180 let WpdMenuItem = _WpdMenuItem;
1181 defineComponent("wpd-menu-item", WpdMenuItem);
1182 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}`;
1183 const ICONS$1 = {
1184 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
1185 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
1186 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"/>',
1187 "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"/>',
1188 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"/>',
1189 reload: (
1190 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
1191 // shared with the other title-bar glyphs. The wrapping `<g>` does
1192 // the math; the inner path is dropped in unmodified so its
1193 // authoring tool can be re-edited and copy-pasted again.
1194 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
1195 // keep the result centered inside the 12×12 viewBox so the
1196 // glyph reads slightly smaller than min/max/close — closer to
1197 // the visual weight of the other title-bar buttons.
1198 '<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>'
1199 ),
1200 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"/>',
1201 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"/>'
1202 };
1203 const _WpdWindowButton = class _WpdWindowButton extends Component {
1204 constructor() {
1205 super(...arguments);
1206 this._activateWired = false;
1207 }
1208 render() {
1209 const iconKey = this.icon || "";
1210 const svgInner = ICONS$1[iconKey] || "";
1211 return html`
1212 <button type="button">
1213 <svg
1214 width="14"
1215 height="14"
1216 viewBox="0 0 12 12"
1217 aria-hidden="true"
1218 focusable="false"
1219 ></svg>
1220 <slot></slot>
1221 </button>
1222 <span data-svg-buffer style="display:none">${svgInner}</span>
1223 `;
1224 }
1225 /**
1226 * After each render, copy the raw SVG markup into the actual
1227 * `<svg>` element. The templater only writes text into slots,
1228 * so we stash the intended markup in a hidden buffer and
1229 * `innerHTML = ` the svg once here — a one-shot post-render
1230 * hook that keeps the declarative template honest.
1231 *
1232 * Also wires up the `wpd-button-activate` CustomEvent that
1233 * fires exactly once per gesture — the canonical contract
1234 * for plugin-registered title-bar buttons. Plugin authors who
1235 * use `addEventListener( 'click', cb )` directly still get
1236 * what they expect (the title bar's drag-handler now excludes
1237 * chrome buttons by class so static clicks land normally),
1238 * but `wpd-button-activate` is the documented surface that
1239 * documents the once-per-gesture contract explicitly. See
1240 * the class-level docblock for rationale.
1241 */
1242 connectedCallback() {
1243 super.connectedCallback();
1244 queueMicrotask(() => this._paintSvg());
1245 queueMicrotask(() => this._wireActivateEvent());
1246 }
1247 attributeChangedCallback(name, oldValue, newValue) {
1248 super.attributeChangedCallback(name, oldValue, newValue);
1249 queueMicrotask(() => this._paintSvg());
1250 }
1251 _paintSvg() {
1252 const root = this.shadowRoot;
1253 if (!root) {
1254 return;
1255 }
1256 const svg = root.querySelector("svg");
1257 const buffer = root.querySelector("[data-svg-buffer]");
1258 if (svg && buffer) {
1259 const markup = buffer.textContent || "";
1260 if (svg.innerHTML !== markup) {
1261 svg.innerHTML = markup;
1262 }
1263 }
1264 }
1265 _wireActivateEvent() {
1266 if (this._activateWired) {
1267 return;
1268 }
1269 const root = this.shadowRoot;
1270 if (!root) {
1271 return;
1272 }
1273 const button = root.querySelector("button");
1274 if (!button) {
1275 return;
1276 }
1277 this._activateWired = true;
1278 button.addEventListener("click", () => {
1279 this.dispatchEvent(
1280 new CustomEvent("wpd-button-activate", {
1281 bubbles: true,
1282 composed: true,
1283 cancelable: true
1284 })
1285 );
1286 });
1287 }
1288 };
1289 _WpdWindowButton.props = ["icon", "active", "danger"];
1290 _WpdWindowButton.styles = [styles$4];
1291 _WpdWindowButton.help = {
1292 title: "Window button",
1293 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.",
1294 status: "stable",
1295 since: "0.9.0",
1296 props: [
1297 {
1298 name: "icon",
1299 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
1300 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
1301 },
1302 {
1303 name: "active",
1304 type: "boolean attribute",
1305 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
1306 },
1307 {
1308 name: "danger",
1309 type: "boolean attribute",
1310 description: "Swaps the hover wash to red — used by the close button."
1311 }
1312 ],
1313 slots: [
1314 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
1315 ],
1316 cssProps: [
1317 { name: "--wpd-btn-color", description: "Resting foreground." },
1318 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
1319 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
1320 { name: "--wpd-btn-bg-active", description: "Pressed background." },
1321 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
1322 { name: "--wpd-btn-outline", description: "Focus outline colour." }
1323 ],
1324 example: html`
1325 <wpd-cluster gap="2">
1326 <wpd-window-button icon="minimize"></wpd-window-button>
1327 <wpd-window-button icon="maximize"></wpd-window-button>
1328 <wpd-window-button icon="menu"></wpd-window-button>
1329 <wpd-window-button icon="close" danger></wpd-window-button>
1330 </wpd-cluster>
1331 `
1332 };
1333 let WpdWindowButton = _WpdWindowButton;
1334 defineComponent("wpd-window-button", WpdWindowButton);
1335 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}}`;
1336 const ICONS = {
1337 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"/>',
1338 close: '<path d="M2.5 2.5l7 7M9.5 2.5l-7 7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>'
1339 };
1340 const _WpdTabChip = class _WpdTabChip extends Component {
1341 render() {
1342 const variant = this.variant || "";
1343 const svgInner = ICONS[variant] || "";
1344 return html`
1345 <button type="button">
1346 <svg
1347 viewBox="0 0 12 12"
1348 aria-hidden="true"
1349 focusable="false"
1350 ></svg>
1351 <slot></slot>
1352 </button>
1353 <span data-svg-buffer style="display:none">${svgInner}</span>
1354 `;
1355 }
1356 connectedCallback() {
1357 super.connectedCallback();
1358 queueMicrotask(() => this._paintSvg());
1359 }
1360 attributeChangedCallback(name, oldValue, newValue) {
1361 super.attributeChangedCallback(name, oldValue, newValue);
1362 queueMicrotask(() => this._paintSvg());
1363 }
1364 _paintSvg() {
1365 const root = this.shadowRoot;
1366 if (!root) {
1367 return;
1368 }
1369 const svg = root.querySelector("svg");
1370 const buffer = root.querySelector("[data-svg-buffer]");
1371 if (svg && buffer) {
1372 const markup = buffer.textContent || "";
1373 if (svg.innerHTML !== markup) {
1374 svg.innerHTML = markup;
1375 }
1376 }
1377 }
1378 };
1379 _WpdTabChip.props = ["variant"];
1380 _WpdTabChip.styles = [styles$3];
1381 _WpdTabChip.help = {
1382 title: "Tab chip",
1383 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.",
1384 status: "stable",
1385 since: "0.9.0",
1386 props: [
1387 {
1388 name: "variant",
1389 type: "'detach' | 'close'",
1390 description: "Selects the built-in SVG icon and the hover wash colour."
1391 }
1392 ],
1393 slots: [
1394 { name: "(default)", description: "Optional custom icon markup when `variant` is omitted." }
1395 ],
1396 example: html`
1397 <wpd-cluster gap="4">
1398 <wpd-tab-chip variant="detach"></wpd-tab-chip>
1399 <wpd-tab-chip variant="close"></wpd-tab-chip>
1400 </wpd-cluster>
1401 `
1402 };
1403 let WpdTabChip = _WpdTabChip;
1404 defineComponent("wpd-tab-chip", WpdTabChip);
1405 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%}`;
1406 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
1407 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
1408 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
1409 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
1410 constructor() {
1411 super(...arguments);
1412 this._autoTimer = null;
1413 this._docListener = null;
1414 }
1415 connectedCallback() {
1416 super.connectedCallback();
1417 if (this.auto !== null) {
1418 this._installAutoListener();
1419 }
1420 }
1421 disconnectedCallback() {
1422 this._removeAutoListener();
1423 if (this._autoTimer !== null) {
1424 window.clearTimeout(this._autoTimer);
1425 this._autoTimer = null;
1426 }
1427 }
1428 attributeChangedCallback(name, oldValue, newValue) {
1429 super.attributeChangedCallback(name, oldValue, newValue);
1430 if (name === "auto" || name === "event") {
1431 this._removeAutoListener();
1432 if (this.auto !== null) {
1433 this._installAutoListener();
1434 }
1435 }
1436 if (name === "phase") {
1437 this._scheduleAutoClear();
1438 const detail = {
1439 phase: this.phase ?? "idle",
1440 error: this.error ?? void 0
1441 };
1442 this.emit("wpd-save-status-change", detail);
1443 }
1444 }
1445 render() {
1446 const phase = this.phase ?? "idle";
1447 const mode = this.mode ?? "dot";
1448 const error = this.error ?? "";
1449 const title = error || this._labelForPhase(phase);
1450 if (title) {
1451 this.setAttribute("title", title);
1452 } else {
1453 this.removeAttribute("title");
1454 }
1455 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
1456 this.setAttribute("role", phase === "failed" ? "alert" : "status");
1457 return html`
1458 <span class="wpd-save-status">
1459 <span class="wpd-save-status__indicator" aria-hidden="true">
1460 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
1461 </span>
1462 ${mode === "pill" ? html`<span class="wpd-save-status__label"
1463 >${this._labelForPhase(phase)}</span
1464 >` : html``}
1465 </span>
1466 `;
1467 }
1468 _renderGlyph(phase) {
1469 if (phase === "saved") {
1470 return _iconCheck();
1471 }
1472 if (phase === "failed") {
1473 return _iconBang();
1474 }
1475 return "";
1476 }
1477 _labelForPhase(phase) {
1478 switch (phase) {
1479 case "pending":
1480 case "saving":
1481 return this["saving-label"] ?? "Saving…";
1482 case "saved":
1483 return this["saved-label"] ?? "Saved";
1484 case "failed": {
1485 const err = this.error ?? "";
1486 return err || "Couldn’t save";
1487 }
1488 default:
1489 return this["idle-label"] ?? "";
1490 }
1491 }
1492 _installAutoListener() {
1493 const eventName = this.event || DEFAULT_EVENT;
1494 this._docListener = (e) => {
1495 const detail = e.detail;
1496 if (!detail || typeof detail.phase !== "string") {
1497 return;
1498 }
1499 this.phase = detail.phase;
1500 if (detail.error) {
1501 this.error = detail.error;
1502 } else if (detail.phase !== "failed" && this.error) {
1503 this.removeAttribute("error");
1504 }
1505 };
1506 document.addEventListener(eventName, this._docListener);
1507 }
1508 _removeAutoListener() {
1509 if (!this._docListener) {
1510 return;
1511 }
1512 const eventName = this.event || DEFAULT_EVENT;
1513 document.removeEventListener(eventName, this._docListener);
1514 this._docListener = null;
1515 }
1516 _scheduleAutoClear() {
1517 if (this._autoTimer !== null) {
1518 window.clearTimeout(this._autoTimer);
1519 this._autoTimer = null;
1520 }
1521 const phase = this.phase ?? "idle";
1522 const ms = this._autoClearMsFor(phase);
1523 if (ms <= 0) {
1524 return;
1525 }
1526 this._autoTimer = window.setTimeout(() => {
1527 this._autoTimer = null;
1528 this.phase = "idle";
1529 }, ms);
1530 }
1531 _autoClearMsFor(phase) {
1532 if (phase === "saved") {
1533 const raw = this["auto-clear-saved-ms"];
1534 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
1535 }
1536 if (phase === "failed") {
1537 const raw = this["auto-clear-failed-ms"];
1538 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
1539 }
1540 return 0;
1541 }
1542 };
1543 _WpdSaveStatus.props = [
1544 "phase",
1545 "mode",
1546 "animation",
1547 "auto",
1548 "event",
1549 "error",
1550 "saving-label",
1551 "saved-label",
1552 "idle-label",
1553 "auto-clear-saved-ms",
1554 "auto-clear-failed-ms"
1555 ];
1556 _WpdSaveStatus.styles = [styles$2];
1557 _WpdSaveStatus.help = {
1558 title: "Save status",
1559 summary: 'Tiny status indicator for "is this change saved yet?" affordances. Three layouts (dot / icon / pill), five phases, optional auto-listen to a save-lifecycle CustomEvent so every input in the panel inherits feedback for free.',
1560 status: "experimental",
1561 since: "0.8.0",
1562 props: [
1563 {
1564 name: "phase",
1565 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
1566 default: "idle",
1567 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
1568 },
1569 {
1570 name: "mode",
1571 type: "'dot' | 'icon' | 'pill'",
1572 default: "dot",
1573 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
1574 },
1575 {
1576 name: "animation",
1577 type: "'pulse' | 'modem'",
1578 default: "pulse",
1579 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."
1580 },
1581 {
1582 name: "auto",
1583 type: "boolean attribute",
1584 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="…"`.'
1585 },
1586 {
1587 name: "event",
1588 type: "string",
1589 default: "desktop-mode-os-settings-save-lifecycle",
1590 description: "CustomEvent name to listen on when `auto` is set."
1591 },
1592 {
1593 name: "error",
1594 type: "string",
1595 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
1596 },
1597 {
1598 name: "saving-label",
1599 type: "string",
1600 default: "Saving…",
1601 description: "Pill-mode label shown during `pending` / `saving`."
1602 },
1603 {
1604 name: "saved-label",
1605 type: "string",
1606 default: "Saved",
1607 description: "Pill-mode label shown during `saved`."
1608 },
1609 {
1610 name: "idle-label",
1611 type: "string",
1612 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
1613 },
1614 {
1615 name: "auto-clear-saved-ms",
1616 type: "integer",
1617 default: "2200",
1618 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
1619 },
1620 {
1621 name: "auto-clear-failed-ms",
1622 type: "integer",
1623 default: "6000",
1624 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
1625 }
1626 ],
1627 events: [
1628 {
1629 name: "wpd-save-status-change",
1630 description: "Fires when the phase changes (manually or via auto-listen).",
1631 detail: "{ phase, error }"
1632 }
1633 ],
1634 cssProps: [
1635 {
1636 name: "--wpd-save-status-bg",
1637 description: "Indicator background color (saving/pending phase)."
1638 },
1639 {
1640 name: "--wpd-save-status-saved-bg",
1641 description: "Indicator background on saved."
1642 },
1643 {
1644 name: "--wpd-save-status-failed-bg",
1645 description: "Indicator background on failed."
1646 },
1647 {
1648 name: "--wpd-save-status-pill-bg",
1649 description: "Pill background (mode=pill)."
1650 },
1651 {
1652 name: "--wpd-save-status-pill-fg",
1653 description: "Pill foreground (mode=pill)."
1654 }
1655 ],
1656 example: html`
1657 <wpd-cluster gap="12">
1658 <wpd-save-status phase="pending"></wpd-save-status>
1659 <wpd-save-status phase="saving"></wpd-save-status>
1660 <wpd-save-status phase="saved"></wpd-save-status>
1661 <wpd-save-status phase="failed"></wpd-save-status>
1662 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
1663 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
1664 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
1665 </wpd-cluster>
1666 `
1667 };
1668 let WpdSaveStatus = _WpdSaveStatus;
1669 defineComponent("wpd-save-status", WpdSaveStatus);
1670 function _iconCheck() {
1671 return html`
1672 <svg
1673 viewBox="0 0 12 12"
1674 aria-hidden="true"
1675 focusable="false"
1676 fill="none"
1677 stroke="currentColor"
1678 stroke-width="2"
1679 stroke-linecap="round"
1680 stroke-linejoin="round"
1681 >
1682 <path d="M2.5 6 L5 8.5 L9.5 4" />
1683 </svg>
1684 `;
1685 }
1686 function _iconBang() {
1687 return html`
1688 <svg
1689 viewBox="0 0 12 12"
1690 aria-hidden="true"
1691 focusable="false"
1692 fill="currentColor"
1693 >
1694 <path
1695 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
1696 />
1697 </svg>
1698 `;
1699 }
1700 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}}`;
1701 const WPD_SPINNER_PRESETS = Object.freeze({
1702 classic: {
1703 sp1: 12,
1704 sp2: 24,
1705 sp3: 40,
1706 a1: 28,
1707 a2: 15,
1708 a3: 8,
1709 gap: 4,
1710 dir2: 1,
1711 dir3: -1,
1712 pulse: "none",
1713 dots: 0
1714 },
1715 comet: {
1716 sp1: 8,
1717 sp2: 14,
1718 sp3: 26,
1719 a1: 50,
1720 a2: 28,
1721 a3: 12,
1722 gap: 3,
1723 dir2: 1,
1724 dir3: 1,
1725 pulse: "none",
1726 dots: 5
1727 },
1728 orbit: {
1729 sp1: 10,
1730 sp2: 10,
1731 sp3: 32,
1732 a1: 50,
1733 a2: 50,
1734 a3: 8,
1735 gap: 5,
1736 dir2: -1,
1737 dir3: -1,
1738 pulse: "opacity",
1739 dots: 3
1740 },
1741 pulse: {
1742 sp1: 6,
1743 sp2: 18,
1744 sp3: 30,
1745 a1: 20,
1746 a2: 12,
1747 a3: 6,
1748 gap: 4,
1749 dir2: 1,
1750 dir3: -1,
1751 pulse: "both",
1752 dots: 8
1753 }
1754 });
1755 const CX = 61.26;
1756 const CY = 61.26;
1757 const DISC_R = 58.453;
1758 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"/>';
1759 const _WpdSpinner = class _WpdSpinner extends Component {
1760 constructor() {
1761 super(...arguments);
1762 this._paintScheduled = false;
1763 }
1764 connectedCallback() {
1765 super.connectedCallback();
1766 this._schedulePaint();
1767 }
1768 render() {
1769 return html`<div class="root" part="root"></div>`;
1770 }
1771 requestUpdate() {
1772 super.requestUpdate();
1773 this._schedulePaint();
1774 }
1775 _schedulePaint() {
1776 if (this._paintScheduled || !this.isConnected) {
1777 return;
1778 }
1779 this._paintScheduled = true;
1780 queueMicrotask(() => {
1781 this._paintScheduled = false;
1782 if (!this.isConnected) {
1783 return;
1784 }
1785 this._paint();
1786 });
1787 }
1788 _paint() {
1789 this._syncCssVars();
1790 const root = this.shadowRoot?.querySelector(
1791 ".root"
1792 );
1793 if (!root) {
1794 return;
1795 }
1796 root.innerHTML = this._buildSvg();
1797 }
1798 /**
1799 * Reflect the color / accent / size attributes onto CSS custom
1800 * properties on the host. Removing the attribute clears the var
1801 * so the default cascades back in.
1802 */
1803 _syncCssVars() {
1804 const sync = (attr, varName, transform) => {
1805 const v = this.getAttribute(attr);
1806 if (v === null) {
1807 this.style.removeProperty(varName);
1808 } else {
1809 this.style.setProperty(
1810 varName,
1811 transform ? transform(v) : v
1812 );
1813 }
1814 };
1815 sync("color", "--wpd-spinner-color");
1816 sync("accent", "--wpd-spinner-accent");
1817 sync(
1818 "size",
1819 "--wpd-spinner-size",
1820 (v) => /^-?\d+(\.\d+)?$/.test(v.trim()) ? `${v}px` : v
1821 );
1822 }
1823 _effectiveConfig() {
1824 const presetName = this.getAttribute("preset") ?? "classic";
1825 const preset = WPD_SPINNER_PRESETS[presetName] ?? WPD_SPINNER_PRESETS.classic;
1826 const num = (attr, fallback) => {
1827 const v = this.getAttribute(attr);
1828 if (v === null) {
1829 return fallback;
1830 }
1831 const n = parseFloat(v);
1832 return Number.isFinite(n) ? n : fallback;
1833 };
1834 const dir = (attr, fallback) => {
1835 const v = this.getAttribute(attr);
1836 if (v === null) {
1837 return fallback;
1838 }
1839 const lc = v.toLowerCase();
1840 if (lc === "-1" || lc === "ccw" || lc === "reverse") {
1841 return -1;
1842 }
1843 return 1;
1844 };
1845 const pulse = () => {
1846 const v = this.getAttribute("pulse");
1847 if (v === "scale" || v === "opacity" || v === "both" || v === "none") {
1848 return v;
1849 }
1850 return preset.pulse;
1851 };
1852 return {
1853 sp1: num("sp1", preset.sp1),
1854 sp2: num("sp2", preset.sp2),
1855 sp3: num("sp3", preset.sp3),
1856 a1: num("a1", preset.a1),
1857 a2: num("a2", preset.a2),
1858 a3: num("a3", preset.a3),
1859 gap: num("gap", preset.gap),
1860 dir2: dir("dir2", preset.dir2),
1861 dir3: dir("dir3", preset.dir3),
1862 pulse: pulse(),
1863 dots: Math.max(0, Math.floor(num("dots", preset.dots)))
1864 };
1865 }
1866 _buildSvg() {
1867 const cfg = this._effectiveConfig();
1868 const label = escAttr(this.getAttribute("label") ?? "Loading");
1869 const pad = cfg.gap * 3 + 14;
1870 const vbMin = -pad;
1871 const vbSize = 122.52 + pad * 2;
1872 const r1 = DISC_R + cfg.gap + 2;
1873 const r2 = r1 + cfg.gap + 2;
1874 const r3 = r2 + cfg.gap + 1.5;
1875 const ring1Anim = `animation: wpd-spinner-spin ${(cfg.sp1 / 10).toFixed(2)}s linear infinite`;
1876 const ring2Anim = `animation: wpd-spinner-spin ${(cfg.sp2 / 10).toFixed(2)}s linear infinite${cfg.dir2 < 0 ? " reverse" : ""}`;
1877 const ring3Anim = `animation: wpd-spinner-spin ${(cfg.sp3 / 10).toFixed(2)}s linear infinite${cfg.dir3 < 0 ? " reverse" : ""}`;
1878 const pspd = (cfg.sp1 * 1.8 / 10).toFixed(1);
1879 const ospd = (cfg.sp1 * 2.3 / 10).toFixed(1);
1880 let pulseStyle = "";
1881 if (cfg.pulse === "scale") {
1882 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite`;
1883 } else if (cfg.pulse === "opacity") {
1884 pulseStyle = `animation: wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
1885 } else if (cfg.pulse === "both") {
1886 pulseStyle = `animation: wpd-spinner-scale ${pspd}s ease-in-out infinite, wpd-spinner-opacity ${ospd}s ease-in-out infinite`;
1887 }
1888 let dotEls = "";
1889 if (cfg.dots > 0) {
1890 const dr = r3 + cfg.gap + 1;
1891 const dc2 = 2 * Math.PI * dr;
1892 const dsz = 1.6;
1893 const dotDur = (cfg.sp1 * 0.65 / 10).toFixed(2);
1894 for (let i = 0; i < cfg.dots; i++) {
1895 const offset = -(i / cfg.dots) * dc2;
1896 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"/>`;
1897 }
1898 }
1899 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>`;
1900 }
1901 };
1902 _WpdSpinner.props = [
1903 "preset",
1904 "size",
1905 "color",
1906 "accent",
1907 "sp1",
1908 "sp2",
1909 "sp3",
1910 "a1",
1911 "a2",
1912 "a3",
1913 "gap",
1914 "dir2",
1915 "dir3",
1916 "pulse",
1917 "dots",
1918 "label"
1919 ];
1920 _WpdSpinner.styles = [styles$1];
1921 _WpdSpinner.help = {
1922 title: "Spinner",
1923 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.",
1924 status: "experimental",
1925 since: "0.6.0",
1926 props: [
1927 {
1928 name: "preset",
1929 type: '"classic" | "comet" | "orbit" | "pulse"',
1930 default: "classic",
1931 description: "Visual personality. Every other attribute defaults to the preset's value and can be overridden individually."
1932 },
1933 {
1934 name: "size",
1935 type: "integer (px) or CSS length",
1936 default: "48",
1937 description: "Sets `--wpd-spinner-size`. Bare numbers are treated as px; pass a CSS length (e.g. `2em`) to opt into ems / rems."
1938 },
1939 {
1940 name: "color",
1941 type: "CSS color",
1942 description: "Disc + ring + dot color. Sets `--wpd-spinner-color`. Default inherits the WP admin theme color."
1943 },
1944 {
1945 name: "accent",
1946 type: "CSS color",
1947 default: "#fff",
1948 description: "Color of the W mark inside the disc. Sets `--wpd-spinner-accent`. Default white — change for dark-on-light or themed marks."
1949 },
1950 {
1951 name: "sp1, sp2, sp3",
1952 type: "integer (deciseconds)",
1953 description: "Per-ring rotation duration in tenths-of-a-second (12 → 1.2s). Higher = slower."
1954 },
1955 {
1956 name: "a1, a2, a3",
1957 type: "integer (0-100)",
1958 description: "Per-ring arc length as a percentage of the ring circumference."
1959 },
1960 {
1961 name: "gap",
1962 type: "integer",
1963 description: "Gap between concentric rings (units approximate to px at 120-viewport)."
1964 },
1965 {
1966 name: "dir2, dir3",
1967 type: '"1" | "-1" | "cw" | "ccw"',
1968 description: "Per-ring direction; ring 1 is always clockwise."
1969 },
1970 {
1971 name: "pulse",
1972 type: '"none" | "scale" | "opacity" | "both"',
1973 description: "Pulse animation applied to the disc + W mark."
1974 },
1975 {
1976 name: "dots",
1977 type: "integer",
1978 description: "Outer trailing dot count. Sensible values: 0, 3, 5, 8."
1979 },
1980 {
1981 name: "label",
1982 type: "string",
1983 default: "Loading",
1984 description: 'Accessible name for the SVG (`role="img"` + `aria-label`).'
1985 }
1986 ],
1987 cssProps: [
1988 { name: "--wpd-spinner-color", default: "var(--wp-admin-theme-color, #21759b)" },
1989 { name: "--wpd-spinner-accent", default: "#fff" },
1990 { name: "--wpd-spinner-size", default: "48px" }
1991 ],
1992 example: html`<wpd-spinner preset="comet" size="80"></wpd-spinner>`
1993 };
1994 let WpdSpinner = _WpdSpinner;
1995 function dasharray(r, pct) {
1996 const c = 2 * Math.PI * r;
1997 const visible = pct / 100 * c;
1998 const gap = c - visible;
1999 return `${visible.toFixed(2)} ${gap.toFixed(2)}`;
2000 }
2001 function escAttr(s) {
2002 return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2003 }
2004 defineComponent("wpd-spinner", WpdSpinner);
2005 const styles = css`:host{display:inline-flex}:host( [ fill-cell ] ){display:flex;width:100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:var( --wpd-button-padding,6px 12px );border-radius:var( --wpd-button-border-radius,6px );font:inherit;font-weight:500;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease;background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid var( --desktop-mode-border,#c3c4c7 ) )}:host( [ fill-cell ] ) button{width:100%;min-height:var( --wpd-button-min-height,44px )}button:disabled{opacity:0.5;cursor:not-allowed}button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.04 ) )}:host( [ variant='primary' ] ) button{background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) );color:var( --wpd-button-fg,#fff );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='primary' ] ) button:hover:not(:disabled ){filter:brightness( 1.06 );background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ variant='secondary' ] ) button{background:var( --wpd-button-bg,rgba( 0,0,0,0.06 ) );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='secondary' ] ) button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.1 ) )}:host( [ variant='danger' ] ) button{background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,#d63638 );border:var( --wpd-button-border,1px solid currentColor )}:host( [ variant='danger' ] ) button:hover:not(:disabled ){background:#d63638;color:#fff}:host( [ variant='link' ] ) button{background:transparent;color:var( --wpd-button-fg,var( --wp-admin-theme-color,#2271b1 ) );border:0;padding:0;text-decoration:underline}:host( [ busy ] ) button{pointer-events:none;opacity:0.75}`;
2006 const _WpdButton = class _WpdButton extends Component {
2007 render() {
2008 const disabled = this.disabled !== null;
2009 const type = this.type || "button";
2010 return html`
2011 <button part="button" type=${type} ?disabled=${disabled}>
2012 <slot></slot>
2013 </button>
2014 `;
2015 }
2016 };
2017 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
2018 _WpdButton.styles = [styles];
2019 _WpdButton.help = {
2020 title: "Button",
2021 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
2022 status: "stable",
2023 since: "0.9.0",
2024 props: [
2025 {
2026 name: "variant",
2027 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
2028 default: "ghost",
2029 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
2030 },
2031 {
2032 name: "disabled",
2033 type: "boolean attribute",
2034 description: "Disable pointer + keyboard interaction and dim the chrome."
2035 },
2036 {
2037 name: "type",
2038 type: "'button' | 'submit' | 'reset'",
2039 default: "button",
2040 description: "Forwarded to the underlying native <button>."
2041 },
2042 {
2043 name: "busy",
2044 type: "boolean attribute",
2045 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
2046 },
2047 {
2048 name: "fill-cell",
2049 type: "boolean attribute",
2050 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
2051 }
2052 ],
2053 slots: [{ name: "(default)", description: "Button label." }],
2054 parts: [{ name: "button", description: "Underlying <button> element." }],
2055 cssProps: [
2056 { name: "--wpd-button-bg", description: "Background color." },
2057 {
2058 name: "--wpd-button-bg-hover",
2059 description: "Hover wash (ghost + secondary variants)."
2060 },
2061 { name: "--wpd-button-fg", description: "Text color." },
2062 { name: "--wpd-button-border", description: "Border shorthand." },
2063 { name: "--wpd-button-border-radius", default: "6px" },
2064 { name: "--wpd-button-padding", default: "6px 12px" },
2065 {
2066 name: "--wpd-button-min-height",
2067 description: "Minimum height when fill-cell is set."
2068 }
2069 ],
2070 example: html`
2071 <wpd-cluster gap="8">
2072 <wpd-button variant="primary">Primary</wpd-button>
2073 <wpd-button variant="secondary">Secondary</wpd-button>
2074 <wpd-button variant="ghost">Ghost</wpd-button>
2075 <wpd-button variant="danger">Danger</wpd-button>
2076 <wpd-button variant="link">Link</wpd-button>
2077 </wpd-cluster>
2078 `
2079 };
2080 let WpdButton = _WpdButton;
2081 defineComponent("wpd-button", WpdButton);
2082 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}`;
2083 const _WpdTextField = class _WpdTextField extends Component {
2084 constructor() {
2085 super(...arguments);
2086 this._revealed = false;
2087 }
2088 connectedCallback() {
2089 super.connectedCallback();
2090 ensureAutoId(this);
2091 }
2092 render() {
2093 const label = this.label || "";
2094 const value = this.value ?? "";
2095 const placeholder = this.placeholder || "";
2096 const disabled = this.disabled !== null;
2097 const readonly = this.readonly !== null;
2098 const declaredAutocomplete = this.autocomplete;
2099 const declaredType = this.type || "text";
2100 const isPassword = declaredType === "password";
2101 let autocomplete = declaredAutocomplete || "off";
2102 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
2103 autocomplete = "new-password";
2104 }
2105 const maxLength = this.maxlength;
2106 const minLength = this.minlength;
2107 const pattern = this.pattern || "";
2108 const name = this.name || "";
2109 const suffix = this.suffix || "";
2110 const invalid = this.invalid !== null;
2111 const reveal = this.reveal !== null;
2112 const isPasswordIntent = declaredType === "password";
2113 const isMasked = isPasswordIntent && !(reveal && this._revealed);
2114 let effectiveType;
2115 if (isPasswordIntent) {
2116 effectiveType = "text";
2117 } else if (reveal && this._revealed) {
2118 effectiveType = "text";
2119 } else {
2120 effectiveType = declaredType;
2121 }
2122 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
2123 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
2124 const hostId = this.id || "wpd-unnamed";
2125 const inputId = `${hostId}__input`;
2126 return html`
2127 ${label ? html`<label
2128 class="wpd-text-field__label"
2129 for=${inputId}
2130 >${label}</label>` : html``}
2131 <span class=${rowClass}>
2132 <input
2133 id=${inputId}
2134 class=${inputClass}
2135 type=${effectiveType}
2136 .value=${value}
2137 placeholder=${placeholder}
2138 ?disabled=${disabled}
2139 ?readonly=${readonly}
2140 autocomplete=${autocomplete}
2141 maxlength=${maxLength ?? ""}
2142 minlength=${minLength ?? ""}
2143 pattern=${pattern}
2144 name=${name}
2145 aria-invalid=${invalid ? "true" : "false"}
2146 aria-label=${label || ""}
2147 @input=${(e) => this._onInput(e)}
2148 @change=${(e) => this._onChange(e)}
2149 @keydown=${(e) => this._onKeyDown(e)}
2150 />
2151 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
2152 ${reveal ? this._renderRevealButton(disabled) : html``}
2153 </span>
2154 `;
2155 }
2156 _renderRevealButton(disabled) {
2157 const label = this._revealed ? "Hide" : "Show";
2158 return html`
2159 <button
2160 type="button"
2161 class="wpd-text-field__reveal"
2162 aria-label=${label}
2163 aria-pressed=${this._revealed ? "true" : "false"}
2164 ?disabled=${disabled}
2165 tabindex="0"
2166 @click=${() => this._onToggleReveal()}
2167 >
2168 ${this._revealed ? _iconEyeOff() : _iconEye()}
2169 </button>
2170 `;
2171 }
2172 _onToggleReveal() {
2173 this._revealed = !this._revealed;
2174 this.requestUpdate();
2175 }
2176 _onInput(e) {
2177 const input = e.target;
2178 this.value = input.value;
2179 this.emit("wpd-input-change", { value: input.value });
2180 }
2181 _onChange(e) {
2182 const input = e.target;
2183 this.emit("wpd-input-commit", { value: input.value });
2184 }
2185 _onKeyDown(e) {
2186 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
2187 const input = e.target;
2188 this.emit("wpd-submit", { value: input.value });
2189 }
2190 }
2191 };
2192 _WpdTextField.props = [
2193 "label",
2194 "value",
2195 "placeholder",
2196 "disabled",
2197 "readonly",
2198 "autocomplete",
2199 "type",
2200 "maxlength",
2201 "minlength",
2202 "pattern",
2203 "name",
2204 "suffix",
2205 "invalid",
2206 "reveal"
2207 ];
2208 _WpdTextField.styles = [textFieldStyles];
2209 _WpdTextField.help = {
2210 title: "Text field",
2211 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.",
2212 status: "stable",
2213 since: "0.5.0",
2214 props: [
2215 { name: "label", type: "string", description: "Visible label above the input." },
2216 { name: "value", type: "string", description: "Current input value; reflected two-way." },
2217 { name: "placeholder", type: "string", description: "Native placeholder string." },
2218 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
2219 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
2220 {
2221 name: "autocomplete",
2222 type: "string",
2223 default: "off",
2224 description: "Forwarded to the native input autocomplete attribute."
2225 },
2226 {
2227 name: "type",
2228 type: "string",
2229 default: "text",
2230 description: "Native input type (text, password, email, search, tel, url)."
2231 },
2232 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
2233 { name: "minlength", type: "integer (string)", description: "Native minlength." },
2234 { name: "pattern", type: "regex string", description: "Native validation pattern." },
2235 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
2236 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
2237 {
2238 name: "invalid",
2239 type: "boolean attribute",
2240 description: "Marks the field aria-invalid and applies the error style."
2241 },
2242 {
2243 name: "reveal",
2244 type: "boolean attribute",
2245 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
2246 }
2247 ],
2248 events: [
2249 {
2250 name: "wpd-input-change",
2251 description: "Fires on every input keystroke.",
2252 detail: "{ value: string }"
2253 },
2254 {
2255 name: "wpd-input-commit",
2256 description: "Fires on the native change event (blur / Enter).",
2257 detail: "{ value: string }"
2258 },
2259 {
2260 name: "wpd-submit",
2261 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
2262 detail: "{ value: string }"
2263 }
2264 ],
2265 cssProps: [
2266 { name: "--desktop-mode-text", description: "Text colour." },
2267 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
2268 { name: "--desktop-mode-border", description: "Input outline." },
2269 { name: "--desktop-mode-window-bg", description: "Input background." }
2270 ],
2271 example: html`
2272 <wpd-stack gap="8">
2273 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
2274 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
2275 </wpd-stack>
2276 `
2277 };
2278 let WpdTextField = _WpdTextField;
2279 defineComponent("wpd-text-field", WpdTextField);
2280 function _iconEye() {
2281 return html`
2282 <svg
2283 viewBox="0 0 16 16"
2284 width="14"
2285 height="14"
2286 fill="none"
2287 stroke="currentColor"
2288 stroke-width="1.5"
2289 stroke-linecap="round"
2290 stroke-linejoin="round"
2291 aria-hidden="true"
2292 focusable="false"
2293 >
2294 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
2295 <circle cx="8" cy="8" r="2" />
2296 </svg>
2297 `;
2298 }
2299 function _iconEyeOff() {
2300 return html`
2301 <svg
2302 viewBox="0 0 16 16"
2303 width="14"
2304 height="14"
2305 fill="none"
2306 stroke="currentColor"
2307 stroke-width="1.5"
2308 stroke-linecap="round"
2309 stroke-linejoin="round"
2310 aria-hidden="true"
2311 focusable="false"
2312 >
2313 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
2314 <circle cx="8" cy="8" r="2" />
2315 <line x1="2" y1="2" x2="14" y2="14" />
2316 </svg>
2317 `;
2318 }
2319 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 )}`;
2320 const optionStyles = css`:host{display:none}`;
2321 const _WpdOption = class _WpdOption extends Component {
2322 render() {
2323 return html``;
2324 }
2325 };
2326 _WpdOption.props = ["value", "disabled"];
2327 _WpdOption.styles = [optionStyles];
2328 _WpdOption.help = {
2329 title: "Option",
2330 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>.",
2331 status: "stable",
2332 since: "0.5.0",
2333 props: [
2334 {
2335 name: "value",
2336 type: "string",
2337 description: "Option identifier read by the parent <wpd-select>."
2338 },
2339 {
2340 name: "disabled",
2341 type: "boolean attribute",
2342 description: "Renders the option disabled in the parent <select>."
2343 }
2344 ],
2345 slots: [
2346 { name: "(default)", description: "Label text read from textContent." }
2347 ]
2348 };
2349 let WpdOption = _WpdOption;
2350 defineComponent("wpd-option", WpdOption);
2351 const _WpdSelect = class _WpdSelect extends Component {
2352 constructor() {
2353 super(...arguments);
2354 this._optionObserver = null;
2355 }
2356 /**
2357 * Declarative item-list setter. Replaces the existing
2358 * `<wpd-option>` children with a fresh set; preserves `value`
2359 * when it still matches, otherwise clears to the placeholder.
2360 *
2361 * Same shape as the setter on `<wpd-segmented>` so callers can
2362 * swap tag names (segmented ↔ select) without touching the
2363 * populate code when an option list outgrows the pill bar.
2364 *
2365 * ```js
2366 * select.items = [
2367 * { value: 'eur', label: 'Euro' },
2368 * { value: 'usd', label: 'US Dollar' },
2369 * ];
2370 * ```
2371 *
2372 * @since 0.5.0
2373 */
2374 set items(list) {
2375 const existing = this.querySelectorAll(":scope > wpd-option");
2376 for (const el of Array.from(existing)) {
2377 el.remove();
2378 }
2379 for (const item of list) {
2380 const opt = document.createElement("wpd-option");
2381 opt.setAttribute("value", item.value);
2382 opt.textContent = item.label;
2383 this.appendChild(opt);
2384 }
2385 const current = this.value;
2386 const stillValid = current !== null && list.some((i) => i.value === current);
2387 if (!stillValid && list.length > 0) {
2388 this.value = list[0].value;
2389 }
2390 this.requestUpdate();
2391 }
2392 connectedCallback() {
2393 super.connectedCallback();
2394 ensureAutoId(this);
2395 this._optionObserver = new MutationObserver(() => this.requestUpdate());
2396 this._optionObserver.observe(this, {
2397 childList: true,
2398 subtree: true,
2399 attributes: true,
2400 attributeFilter: ["value", "disabled"],
2401 characterData: true
2402 });
2403 }
2404 disconnectedCallback() {
2405 this._optionObserver?.disconnect();
2406 this._optionObserver = null;
2407 }
2408 render() {
2409 const label = this.label || "";
2410 const current = this.value;
2411 const placeholder = this.placeholder || "";
2412 const disabled = this.disabled !== null;
2413 const name = this.name || "";
2414 if (label) {
2415 this.setAttribute("aria-label", label);
2416 } else {
2417 this.removeAttribute("aria-label");
2418 }
2419 const selectAriaLabel = label || placeholder;
2420 const options = this._readOptions();
2421 const hostId = this.id || "wpd-unnamed";
2422 const selectId = `${hostId}__input`;
2423 return html`
2424 ${label ? html`<label
2425 class="wpd-select__label"
2426 for=${selectId}
2427 >${label}</label>` : html``}
2428 <span class="wpd-select__wrap">
2429 <select
2430 id=${selectId}
2431 ?disabled=${disabled}
2432 aria-label=${selectAriaLabel}
2433 name=${name}
2434 @change=${(e) => this._onChange(e)}
2435 >
2436 ${placeholder && !current ? html`<option value="" disabled selected>
2437 ${placeholder}
2438 </option>` : html``}
2439 ${options.map(
2440 (o) => html`
2441 <option
2442 value=${o.value}
2443 ?disabled=${o.disabled}
2444 ?selected=${o.value === current}
2445 >
2446 ${o.label}
2447 </option>
2448 `
2449 )}
2450 </select>
2451 <!--
2452 Inline SVG — the previous dashicons-classed span
2453 never painted because the global Dashicons font
2454 stylesheet cannot cross the shadow-root boundary.
2455 An inline SVG lives inside the shadow tree, inherits
2456 currentColor via the stroke attribute, and needs
2457 no external CSS.
2458 -->
2459 <svg
2460 class="wpd-select__chevron"
2461 viewBox="0 0 12 12"
2462 width="12"
2463 height="12"
2464 aria-hidden="true"
2465 focusable="false"
2466 >
2467 <path
2468 d="M3 5l3 3 3-3"
2469 stroke="currentColor"
2470 stroke-width="1.4"
2471 stroke-linecap="round"
2472 stroke-linejoin="round"
2473 fill="none"
2474 ></path>
2475 </svg>
2476 </span>
2477 `;
2478 }
2479 _readOptions() {
2480 const out = [];
2481 const children = this.querySelectorAll(":scope > wpd-option");
2482 for (const child of Array.from(children)) {
2483 const value = child.getAttribute("value");
2484 if (value === null) {
2485 continue;
2486 }
2487 out.push({
2488 value,
2489 label: (child.textContent || value).trim(),
2490 disabled: child.hasAttribute("disabled")
2491 });
2492 }
2493 return out;
2494 }
2495 _onChange(e) {
2496 const sel = e.target;
2497 const next = sel.value;
2498 this.value = next;
2499 this.emit("wpd-pick", { value: next });
2500 }
2501 };
2502 _WpdSelect.props = [
2503 "value",
2504 "label",
2505 "placeholder",
2506 "disabled",
2507 "name"
2508 ];
2509 _WpdSelect.styles = [selectStyles];
2510 _WpdSelect.help = {
2511 title: "Select",
2512 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.",
2513 status: "stable",
2514 since: "0.5.0",
2515 props: [
2516 {
2517 name: "value",
2518 type: "string",
2519 description: "Currently selected option value."
2520 },
2521 {
2522 name: "label",
2523 type: "string",
2524 description: "Visible label rendered above the select and forwarded to the native control as aria-label."
2525 },
2526 {
2527 name: "placeholder",
2528 type: "string",
2529 description: "Disabled leading option shown when no value is set."
2530 },
2531 {
2532 name: "disabled",
2533 type: "boolean attribute",
2534 description: "Disables the native select and dims the chrome."
2535 },
2536 {
2537 name: "name",
2538 type: "string",
2539 description: "Forwarded to the native <select name=…> for form submission."
2540 }
2541 ],
2542 slots: [
2543 { name: "(default)", description: '<wpd-option value="…"> children.' }
2544 ],
2545 events: [
2546 {
2547 name: "wpd-pick",
2548 description: "Fires when the user picks a new option.",
2549 detail: "{ value: string }"
2550 }
2551 ],
2552 cssProps: [
2553 { name: "--desktop-mode-text", description: "Label + value colour." },
2554 { name: "--desktop-mode-muted", description: "Placeholder + chevron colour." }
2555 ],
2556 example: html`
2557 <wpd-select value="eur" label="Currency">
2558 <wpd-option value="eur">Euro</wpd-option>
2559 <wpd-option value="usd">US Dollar</wpd-option>
2560 <wpd-option value="jpy">Japanese Yen</wpd-option>
2561 </wpd-select>
2562 `
2563 };
2564 let WpdSelect = _WpdSelect;
2565 defineComponent("wpd-select", WpdSelect);
2566 })();
2567