| 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 |
>×</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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">"); |
| 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 |
|