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