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

window-chrome.css in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.8, at assets/css/window-chrome.css

760 lines 25.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Desktop Mode — Window chrome.
3 *
4 * Everything that renders the base window frame: title bar, icon +
5 * title text, control buttons, focused / unfocused colour variants,
6 * the ⋯ actions menu panel, screen-meta buttons, the tab strip (for
7 * submenu + external tabs), the window body, the primary iframe, and
8 * the native-body variant. State-driven rules (dragging / resizing /
9 * maximized / fullscreen / overview / minimized / closing) live in
10 * sibling files loaded via `windows.css`.
11 *
12 * @since 6.9.0
13 */
14
15 /* Base window. */
16 .desktop-mode-window {
17 /*
18 * border-box sizing is essential for pixel-perfect maximize.
19 * `Window.maximize()` sets inline `width = parent.clientWidth`,
20 * which is the area's inner width. Under content-box sizing, the
21 * window's 1px border would add 2 extra pixels to the rendered
22 * element, pushing its right + bottom borders past the
23 * overflow-hidden boundary of `.desktop-mode-area` and making them
24 * look clipped/offscreen. With border-box, width/height INCLUDE
25 * the border, so `width = clientWidth` fits exactly.
26 *
27 * WP admin CSS does set `*, *::before, *::after { box-sizing:
28 * border-box }` globally, but setting it explicitly here
29 * insulates us from stylesheets or embeds that might reset the
30 * universal rule — the correctness of maximize geometry is too
31 * important to depend on ambient CSS.
32 */
33 box-sizing: border-box;
34 position: absolute;
35 display: flex;
36 flex-direction: column;
37 background: var(--desktop-mode-window-bg);
38 border: 1px solid var(--desktop-mode-window-border);
39 border-radius: var(--desktop-mode-window-radius);
40 box-shadow: var(--desktop-mode-window-shadow);
41 overflow: hidden;
42 min-width: 320px;
43 min-height: 200px;
44 transform-origin: center center;
45 transition:
46 left 0.25s cubic-bezier(0.2, 0, 0.2, 1),
47 top 0.25s cubic-bezier(0.2, 0, 0.2, 1),
48 width 0.25s cubic-bezier(0.2, 0, 0.2, 1),
49 height 0.25s cubic-bezier(0.2, 0, 0.2, 1),
50 border-radius 0.25s ease,
51 transform 0.2s ease,
52 opacity 0.2s ease,
53 box-shadow 0.2s ease;
54 }
55
56 /*
57 * Suppress the left/top/width/height transition during drag or resize —
58 * otherwise every pointer move lerps and the window lags the cursor.
59 *
60 * Also during a viewport reflow (`--reflowing`) — when the browser
61 * window is being dragged smaller, the ResizeObserver fires many
62 * times per second and each style write would restart the 250 ms
63 * transition, leaving maximized / snapped windows ~250 ms behind the
64 * viewport the whole time. The class is added by
65 * `reflowStatefulWindows` and cleared ~140 ms after the last tick.
66 */
67 .desktop-mode-window--dragging,
68 .desktop-mode-window--resizing,
69 .desktop-mode-window--reflowing {
70 transition: none;
71 }
72
73 /*
74 * Snap-to-grid drag/resize: re-enable a SHORT transition so each
75 * cell-to-cell jump animates smoothly instead of teleporting. The
76 * `--snap-drag` class is added by the window class only when snap
77 * is on at drag/resize start; it composes with `--dragging` /
78 * `--resizing` and the source-order ensures it wins. Kept short
79 * (90 ms) so the window still feels glued to the cursor — anything
80 * longer reads as lag.
81 */
82 .desktop-mode-window--snap-drag {
83 transition:
84 left 0.09s ease-out,
85 top 0.09s ease-out,
86 width 0.09s ease-out,
87 height 0.09s ease-out;
88 }
89
90 /* Focused window gets elevated shadow and distinct title bar. */
91 .desktop-mode-window--focused {
92 box-shadow: var(--desktop-mode-window-shadow-focused);
93 }
94
95 /* Title bar. */
96 .desktop-mode-window__titlebar {
97 position: relative;
98 /* Promote the titlebar into its own stacking context above the
99 * body. Without this, the body (which comes later in source order
100 * and inherits z-auto) wins document-order overlap battles —
101 * notably any sticky-positioned content inside the body (e.g. the
102 * native Posts table's sticky header at z-index 20–40) would
103 * paint over the titlebar's ⋯ menu popover. */
104 z-index: 21;
105 display: flex;
106 align-items: center;
107 height: var(--desktop-mode-titlebar-height);
108 padding: 0 8px;
109 background: var(--desktop-mode-titlebar-bg);
110 color: var(--desktop-mode-titlebar-color);
111 cursor: default;
112 user-select: none;
113 flex-shrink: 0;
114 gap: 8px;
115 }
116
117 .desktop-mode-window--focused .desktop-mode-window__titlebar {
118 background: var(--desktop-mode-titlebar-bg-focused);
119 color: var(--desktop-mode-titlebar-color-focused);
120 }
121
122 /* Window icon in title bar. */
123 .desktop-mode-window__icon {
124 font-size: 18px;
125 width: 18px;
126 height: 18px;
127 flex-shrink: 0;
128 }
129
130 /* Activity indicator slot — sits between the icon and the title.
131 * Reserves a fixed width so the indicator's blink animation can't
132 * shift the title text horizontally. The inner `<wpd-save-status>`
133 * paints a 12px modem-style dot (always visible, accent-colored).
134 *
135 * `--wp-admin-theme-color` is forwarded as `color` on the host so
136 * the inner shadow-DOM stylesheet's `currentColor` references
137 * (used in the box-shadow glow) resolve to the live accent. */
138 .desktop-mode-window__activity {
139 display: inline-flex;
140 align-items: center;
141 justify-content: center;
142 width: 14px;
143 height: 14px;
144 flex-shrink: 0;
145 margin-inline-start: 6px;
146 margin-inline-end: 4px;
147 color: var(--wp-admin-theme-color, #2271b1);
148 }
149
150 /* Window title text. */
151 .desktop-mode-window__title {
152 flex: 1;
153 overflow: hidden;
154 text-overflow: ellipsis;
155 white-space: nowrap;
156 font-size: 14px;
157 font-weight: 500;
158 line-height: var(--desktop-mode-titlebar-height);
159 }
160
161 /* Window control buttons container. */
162 .desktop-mode-window__controls {
163 display: flex;
164 gap: 4px;
165 align-items: center;
166 flex-shrink: 0;
167 }
168
169 /*
170 * Layer-3 slot hosts.
171 *
172 * Most in-titlebar slots wrap canonical chrome elements without
173 * contributing to the flex layout themselves — `display: contents`
174 * removes the wrapper's box so the inner element (icon span with
175 * `flex-shrink: 0`, before/after spacers, etc.) keeps its
176 * original layout role.
177 *
178 * The `title` slot is the exception: it's the flex-grow region of
179 * the title bar (`flex: 1`). When a plugin replaces the default
180 * title with custom HTML or a render callback, we want the new
181 * content to inherit the same "fill the remaining horizontal
182 * space" behaviour — so the slot itself owns `flex: 1` and the
183 * default title element's own `flex: 1` cooperates inside the
184 * nested flex.
185 *
186 * before/after-titlebar slots are flex-column children of
187 * `.desktop-mode-window` (siblings of the title bar). They're real
188 * boxes — plugins can paint backgrounds, padding, borders. Empty
189 * hosts collapse via `:empty` so they take no space until populated.
190 *
191 * @since 0.6.0
192 */
193 .desktop-mode-window__slot--before-icon,
194 .desktop-mode-window__slot--icon,
195 .desktop-mode-window__slot--after-title,
196 .desktop-mode-window__slot--before-controls,
197 .desktop-mode-window__slot--after-controls {
198 display: contents;
199 }
200
201 .desktop-mode-window__slot--title {
202 flex: 1;
203 min-width: 0;
204 display: flex;
205 align-items: center;
206 overflow: hidden;
207 }
208
209 .desktop-mode-window__slot--before-titlebar,
210 .desktop-mode-window__slot--after-titlebar {
211 display: block;
212 flex-shrink: 0;
213 }
214
215 .desktop-mode-window__slot--before-titlebar:empty,
216 .desktop-mode-window__slot--after-titlebar:empty {
217 display: none;
218 }
219
220 /* ---------------------------------------------------------------
221 * Window control buttons.
222 *
223 * Flat, icon-only buttons styled to feel at home in the WordPress
224 * admin: transparent by default, subtle tinted hover, focus ring in
225 * the active admin theme color, inline SVG icons inheriting
226 * currentColor so they adapt to focused / unfocused title bars.
227 *
228 * The close button gets a destructive red wash on hover only —
229 * never as a default state — so it reads as safe until interacted
230 * with, matching the WordPress admin's pattern of reserving color
231 * for semantic signal.
232 * --------------------------------------------------------------- */
233 /*
234 * Title-bar chrome buttons render as `<wpd-window-button>`.
235 * Shadow DOM can't reach across the window's focus class, so we
236 * drive the coloring via custom properties that inherit through
237 * the boundary. The component reads `--wpd-btn-color`,
238 * `--wpd-btn-bg-hover`, etc. and paints accordingly.
239 */
240 .desktop-mode-window--focused {
241 --wpd-btn-color: rgba( 255, 255, 255, 0.7 );
242 --wpd-btn-color-hover: #fff;
243 --wpd-btn-bg-hover: rgba( 255, 255, 255, 0.18 );
244 --wpd-btn-bg-active: rgba( 255, 255, 255, 0.25 );
245 --wpd-btn-outline: rgba( 255, 255, 255, 0.65 );
246 --wpd-btn-danger-hover: #d63638;
247 }
248
249 .desktop-mode-window:not( .desktop-mode-window--focused ) {
250 --wpd-btn-color: rgba( 0, 0, 0, 0.45 );
251 --wpd-btn-color-hover: rgba( 0, 0, 0, 0.85 );
252 --wpd-btn-bg-hover: rgba( 0, 0, 0, 0.08 );
253 --wpd-btn-bg-active: rgba( 0, 0, 0, 0.12 );
254 --wpd-btn-outline: var( --wp-admin-theme-color, #2271b1 );
255 --wpd-btn-danger-hover: #d63638;
256 }
257
258 /* ---------------------------------------------------------------
259 * Title-bar actions menu (leading edge, before icon + title).
260 *
261 * Trigger is a standard `.desktop-mode-window__btn` — inherits focused /
262 * unfocused coloring from the existing control-button rules. The only
263 * bespoke rule is margin: it sits at the leading edge so a small trailing
264 * gap separates it from the icon without affecting overall title-bar gap.
265 *
266 * Panel is an absolute dropdown anchored to the title bar (which is now
267 * position: relative). Visibility is driven by the `hidden` attribute on
268 * the element itself — no separate `--open` class to keep in sync with
269 * aria-expanded.
270 * --------------------------------------------------------------- */
271 /*
272 * ⋯ button sits between the screen-meta cluster and the window
273 * controls, as the "last page-level chrome" item. When present it
274 * takes over the divider duty: screen-meta drops its own trailing
275 * divider, and the controls container gains a leading one — so a
276 * single clean line always sits between page chrome and window
277 * chrome regardless of which combination is visible.
278 */
279 .desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__screen-meta {
280 margin-inline-end: 0;
281 padding-inline-end: 0;
282 border-inline-end: none;
283 }
284
285 .desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__controls {
286 margin-inline-start: 8px;
287 padding-inline-start: 8px;
288 border-inline-start: 1px solid rgba(255, 255, 255, 0.15);
289 }
290
291 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__controls {
292 border-inline-start-color: rgba(0, 0, 0, 0.1);
293 }
294
295 /*
296 * Menu popover lives inside the title bar's absolute-positioned
297 * coordinate system — positioning is caller-specific and stays
298 * outer. Background, border, items, checkbox styling all moved
299 * into `<wpd-menu>` / `<wpd-menu-item>`.
300 */
301 .desktop-mode-window__menu-panel {
302 position: absolute;
303 top: calc( var( --desktop-mode-titlebar-height ) + 2px );
304 inset-inline-end: 60px;
305 z-index: 2;
306 }
307
308 /* ---------------------------------------------------------------
309 * Screen Meta buttons (Screen Options / Help) in the title bar.
310 * Positioned right after the title text, visually separated from
311 * the close / maximize / minimize cluster by a subtle divider.
312 * Sized to meet WCAG 2.2 target size (24x24 minimum).
313 * --------------------------------------------------------------- */
314 .desktop-mode-window__screen-meta {
315 display: flex;
316 gap: 4px;
317 align-items: center;
318 flex-shrink: 0;
319 margin-inline-end: 8px;
320 padding-inline-end: 8px;
321 border-inline-end: 1px solid rgba(255, 255, 255, 0.15);
322 }
323
324 /* Hide the divider when there are no screen-meta buttons. */
325 .desktop-mode-window__screen-meta:empty {
326 display: none;
327 }
328
329 .desktop-mode-window__meta-btn {
330 display: flex;
331 align-items: center;
332 justify-content: center;
333 width: 28px;
334 height: 28px;
335 border: none;
336 border-radius: 6px;
337 cursor: pointer;
338 padding: 0;
339 background: transparent;
340 transition: background-color 0.15s ease, color 0.15s ease;
341 }
342
343 .desktop-mode-window__meta-btn .dashicons {
344 font-size: 18px;
345 width: 18px;
346 height: 18px;
347 }
348 /* The flex-centring override for the dashicon glyph lives in
349 * `windows.css` rather than here. `windows.css` is mtime-stamped
350 * (per `desktop_mode_register_assets` in includes/assets.php) so
351 * its URL changes whenever it does, but `@import`-ed sub-sheets
352 * like this one are fetched without a version query and the
353 * browser will serve a stale cached copy across edits to this
354 * file. Co-locating cache-sensitive overrides with the parent
355 * stylesheet is the rule of the road here. */
356
357 /* Focused window: meta buttons visible. */
358 .desktop-mode-window--focused .desktop-mode-window__meta-btn {
359 color: rgba(255, 255, 255, 0.65);
360 }
361 .desktop-mode-window--focused .desktop-mode-window__meta-btn:hover {
362 color: #fff;
363 background: rgba(255, 255, 255, 0.18);
364 }
365 .desktop-mode-window--focused .desktop-mode-window__meta-btn:focus-visible {
366 color: #fff;
367 background: rgba(255, 255, 255, 0.18);
368 outline: 2px solid rgba(255, 255, 255, 0.6);
369 outline-offset: 1px;
370 }
371 .desktop-mode-window--focused .desktop-mode-window__meta-btn--active {
372 color: #fff;
373 background: rgba(255, 255, 255, 0.25);
374 }
375
376 /* Unfocused window: divider and buttons adapt to light title bar. */
377 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__screen-meta {
378 border-inline-end-color: rgba(0, 0, 0, 0.1);
379 }
380 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn {
381 color: rgba(0, 0, 0, 0.3);
382 }
383 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn:hover {
384 color: rgba(0, 0, 0, 0.6);
385 background: rgba(0, 0, 0, 0.08);
386 }
387 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn--active {
388 color: rgba(0, 0, 0, 0.6);
389 background: rgba(0, 0, 0, 0.12);
390 }
391
392 /* ---------------------------------------------------------------
393 * Tab strip — submenu navigation rendered in the parent shell, just
394 * below the title bar. Each tab swaps the iframe URL in place; no
395 * new window opens. Horizontally scrollable on narrow windows so the
396 * same component works on tablet and mobile shells unchanged.
397 * --------------------------------------------------------------- */
398 .desktop-mode-window__tabs {
399 display: flex;
400 flex-shrink: 0;
401 gap: 2px;
402 padding: 0 8px;
403 background: var(--desktop-mode-tabs-bg, #f6f7f7);
404 border-bottom: 1px solid var(--desktop-mode-window-border);
405 overflow-x: auto;
406 overflow-y: hidden;
407 scrollbar-width: thin;
408 /*
409 * Soft edge fades so overflowing tabs tell the user "scroll for
410 * more." The mask is always applied — when the strip doesn't
411 * overflow, the faded ends are empty area beyond the last tab so
412 * the cosmetic is invisible. When it DOES overflow, the last-
413 * visible tab fades under the mask, creating the affordance.
414 * `overscroll-behavior-x: contain` keeps horizontal trackpad
415 * swipes inside the strip instead of triggering browser back.
416 */
417 mask-image: linear-gradient(
418 to right,
419 transparent 0,
420 #000 16px,
421 #000 calc(100% - 16px),
422 transparent 100%
423 );
424 -webkit-mask-image: linear-gradient(
425 to right,
426 transparent 0,
427 #000 16px,
428 #000 calc(100% - 16px),
429 transparent 100%
430 );
431 overscroll-behavior-x: contain;
432 }
433
434 /*
435 * When JS assigns the --has-overflow marker (future enhancement), the
436 * mask is stronger. For now the CSS-only mask is subtle enough to be
437 * invisible on non-overflowing strips — mask cut-offs land outside the
438 * flex content.
439 */
440
441 .desktop-mode-window__tab {
442 flex-shrink: 0;
443 display: inline-flex;
444 align-items: center;
445 height: 32px;
446 padding: 0 12px;
447 border: none;
448 background: transparent;
449 color: var(--desktop-mode-tabs-color, #50575e);
450 font: inherit;
451 font-size: 12px;
452 line-height: 1;
453 cursor: pointer;
454 border-bottom: 2px solid transparent;
455 margin-bottom: -1px;
456 white-space: nowrap;
457 transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
458 }
459
460 .desktop-mode-window__tab:hover {
461 color: var(--wp-admin-theme-color, #2271b1);
462 background: rgba(0, 0, 0, 0.03);
463 }
464
465 .desktop-mode-window__tab:focus-visible {
466 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
467 outline-offset: -2px;
468 }
469
470 .desktop-mode-window__tab--active {
471 color: var(--wp-admin-theme-color, #2271b1);
472 border-bottom-color: var(--wp-admin-theme-color, #2271b1);
473 font-weight: 600;
474 }
475
476 /*
477 * External sub-tab. Same shape as a submenu tab, plus two inline
478 * chips: detach (↗) and close (×). Chips are painted as spans
479 * (rather than nested buttons — nested interactive elements are a
480 * pain for a11y). The tab's click handler routes chip clicks via
481 * the `data-tab-action` dataset attribute.
482 */
483 .desktop-mode-window__tab--external {
484 /* Slightly wider gap between the label and the chip cluster, and
485 * more breathing room at the trailing edge so the chips aren't
486 * flush against the tab boundary. */
487 gap: 10px;
488 padding-inline-end: 4px;
489 }
490
491 .desktop-mode-window__tab-label {
492 max-width: 180px;
493 overflow: hidden;
494 text-overflow: ellipsis;
495 white-space: nowrap;
496 }
497
498 /* External-tab action chips moved to `<wpd-tab-chip>`. Spacing
499 * between chips handled by a single host-level rule since the
500 * component doesn't know about its siblings. */
501 wpd-tab-chip + wpd-tab-chip {
502 margin-inline-start: 4px;
503 }
504
505 /* Window body: contains the iframe. */
506 .desktop-mode-window__body {
507 flex: 1;
508 position: relative;
509 overflow: hidden;
510 }
511
512 /* Iframe fills the window body. */
513 .desktop-mode-window__iframe {
514 width: 100%;
515 height: 100%;
516 border: none;
517 display: block;
518 background: var(--desktop-mode-window-bg);
519 opacity: 1;
520 transition: opacity 0.25s ease;
521 }
522
523 /* Cross-window drag drop-overlay rules live in windows.css, NOT here.
524 * Putting them in this @import'd sub-sheet means browsers can serve a
525 * stale cached copy even when the parent windows.css has been cache-
526 * busted by mtime, leaving the drag stuck at the iframe boundary
527 * until the user manually clears their stylesheet cache. Keep them
528 * co-located with the @import declarations themselves. */
529
530 /*
531 * Loading-state overlay — painted by `src/window/dom.ts` into
532 * every window's body and removed once the body's content reports
533 * ready. The `<wpd-spinner>` inside is sized responsively against
534 * the window's width via `clamp(96px, 14vw, 192px)` so a tiny
535 * popover gets a small spinner and a maximized window gets a big
536 * one. Placed above the body content (iframe / native render
537 * output) and kept aria-hidden so the spinner's own SR-label is
538 * the only loading announcement.
539 *
540 * `transition-delay` on the overlay's fade-in means a render
541 * that lands within ~120ms never paints the spinner — the
542 * overlay only becomes visible when there's actually a wait
543 * worth surfacing. This keeps the affordance from flashing on
544 * fast local-dev loads while still covering the slow production
545 * iframe boots that motivated it.
546 */
547 .desktop-mode-window__loading {
548 position: absolute;
549 inset: 0;
550 display: flex;
551 align-items: center;
552 justify-content: center;
553 pointer-events: none;
554 background: var(--desktop-mode-window-bg);
555 opacity: 0;
556 /* Default (loaded → loading-removed) — fade out immediately. */
557 transition: opacity 0.25s ease;
558 z-index: 1;
559 }
560
561 .desktop-mode-window__body--loading > .desktop-mode-window__iframe,
562 .desktop-mode-window__body--loading > :not(.desktop-mode-window__loading) {
563 opacity: 0;
564 transition: opacity 0.25s ease;
565 }
566
567 .desktop-mode-window__body--loading > .desktop-mode-window__loading {
568 opacity: 1;
569 /* Entry transition with a small delay — loads that finish in
570 * under ~120ms never paint the spinner, so fast local-dev /
571 * hot-cache fires don't flash. Slow production iframe boots
572 * still see the spinner. */
573 transition-delay: 0.12s;
574 }
575
576 @media ( prefers-reduced-motion: reduce ) {
577 .desktop-mode-window__iframe,
578 .desktop-mode-window__loading,
579 .desktop-mode-window__body--loading > .desktop-mode-window__iframe,
580 .desktop-mode-window__body--loading > :not(.desktop-mode-window__loading) {
581 transition: none;
582 }
583 }
584
585 /*
586 * External sub-tab iframes stack in the same body as the primary
587 * iframe. Only one is visible at a time (managed by `switchToTab`
588 * via `style.display`). Absolute positioning so they don't push the
589 * primary iframe around when added to the DOM; `display: none` on
590 * inactive iframes is set inline by the JS.
591 */
592 .desktop-mode-window__iframe--external {
593 position: absolute;
594 inset: 0;
595 }
596
597 /*
598 * Native window body — fills the window but lets its contents scroll
599 * independently. Iframe bodies use `overflow: hidden` so the iframe
600 * controls its own scroll; native bodies render real document content
601 * that needs the parent to manage overflow.
602 */
603 .desktop-mode-window__body--native {
604 overflow: auto;
605 color: #1d2327;
606 background: var(--desktop-mode-window-bg);
607 }
608
609 /*
610 * Highlight rings. Toggled from JS by `Window.setHighlight()` —
611 * used by plugins that need to point at a window from outside it
612 * (e.g. a "connect to" dropdown that previews candidate windows on
613 * hover). `--wp-window-highlight-color` is overridable per-instance
614 * via `setHighlight( mode, { color } )` or globally via the
615 * variable.
616 */
617 .wp-window {
618 --wp-window-highlight-color: var(
619 --wp-admin-theme-color, #2271b1
620 );
621 }
622 .wp-window--highlight-preview {
623 box-shadow: 0 0 0 3px var( --wp-window-highlight-color ),
624 0 0 24px 4px var( --wp-window-highlight-color );
625 transition: box-shadow 0.12s ease;
626 z-index: 9999;
627 }
628 .wp-window--highlight-persistent {
629 box-shadow: 0 0 0 2px var( --wp-window-highlight-color );
630 transition: box-shadow 0.12s ease;
631 }
632
633 /*
634 * Slots for plugin-registered title-bar buttons. Empty by default
635 * — `display: contents` hides them entirely from layout when no
636 * plugin has registered a button for the window. When buttons ARE
637 * present, they sit inline next to the title (left slot) or just
638 * before the window controls (right slot).
639 */
640 .desktop-mode-window__custom-buttons {
641 display: contents;
642 }
643 .desktop-mode-window__btn--custom {
644 margin: 0 2px;
645 }
646 /*
647 * Plugin-supplied icons render in the host's light DOM so the
648 * global Dashicons stylesheet reaches them (shadow DOM doesn't
649 * inherit page CSS).
650 *
651 * Dashicons render at their native 20×20 — that's the size the
652 * font is hinted for and the only one where glyph metrics put
653 * the visual centre on the geometric centre. Earlier we tried
654 * down-scaling them to 14×14 to match the built-in chrome icons,
655 * but font-size: 14 on a Dashicon shifts the glyph 1–2 pixels
656 * off centre because the font's ascent/descent ratio doesn't
657 * scale linearly. The 30×30 button has 5px of padding around a
658 * 20×20 glyph — comfortable, and visually consistent with the
659 * 14×14 chrome icons because both are flex-centred in the same
660 * box.
661 *
662 * For inline-SVG icons that plugin authors ship without explicit
663 * width/height, we DO clamp to 18×18 — those are the cases where
664 * a bare `<svg>` would otherwise size to its viewBox or default
665 * to 300×150 (the spec default) and overflow the button.
666 */
667 .desktop-mode-window__btn--custom svg:not( [ width ] ):not( [ height ] ) {
668 width: 18px;
669 height: 18px;
670 display: block;
671 }
672
673 /* ----------------------------------------------------------------
674 * Custom-chrome marker — hides every framework-shipped titlebar
675 * child while a plugin's chrome is mounted.
676 *
677 * The window gets the `desktop-mode-window--custom-chrome` class the
678 * moment `mountWindowChrome` succeeds (BEFORE the plugin's
679 * `render()` runs). Default children carry the
680 * `data-desktop-mode-default-chrome` attribute stamped at element-
681 * creation time. The combination is a load-bearing guarantee:
682 * even if the plugin's render() doesn't clear `titlebar.innerHTML`,
683 * even if a plugin's destroy() leaks the standard chrome back into
684 * place, even mid-fade during a window close — the default chrome
685 * NEVER becomes visible while the marker class is active.
686 *
687 * The class is removed only when the chrome is swapped to standard
688 * (in `Window.remountWindowChrome`); during a window close it
689 * persists until `element.remove()` runs in `onDone()`, which is
690 * after the fade has reached opacity 0 — so the user never sees
691 * the swap.
692 *
693 * @since 0.18.0
694 * ---------------------------------------------------------------- */
695
696 .desktop-mode-window--custom-chrome
697 > .desktop-mode-window__titlebar
698 > [ data-desktop-mode-default-chrome ] {
699 display: none !important;
700 }
701
702 /* ---------------------------------------------------------------
703 * Reload button feedback.
704 *
705 * Click feedback is decoupled from the loading state:
706 *
707 * 1. On click, the icon performs a single 360° rotation with a
708 * fast-start / slow-end ease curve — confirms the user's
709 * gesture independently of how long the page takes to load.
710 * The `--spinning` class is added by the click handler and
711 * removed on `animationend` so a subsequent click restarts
712 * the animation cleanly.
713 *
714 * 2. While the window body is in the `--loading` state (set by
715 * `markContentLoading()` and cleared on the iframe's
716 * `desktop-mode-ready` postMessage), the button is dimmed and
717 * non-interactive so a second click can't desync the
718 * chromeless bridge's loading handshake.
719 *
720 * `:has()` is used instead of mirroring the `--loading` modifier
721 * onto the window root because upstream's loading API only
722 * decorates the body element. Browsers without :has() (very old)
723 * fall back to a static dimmed button — feature, not bug.
724 * --------------------------------------------------------------- */
725
726 .desktop-mode-window:has( .desktop-mode-window__body--loading )
727 .desktop-mode-window__btn--reload {
728 pointer-events: none;
729 opacity: 0.55;
730 }
731
732 /*
733 * Click feedback animation. The custom easing — `cubic-bezier( 0.05,
734 * 0.7, 0.1, 1 )` — is a steeper-than-default ease-out: ~70% of the
735 * rotation lands in the first 30% of the duration, then it decelerates.
736 * Reads as "snap, then settle" rather than the linear glide of a
737 * loading-spinner, reinforcing that this is a one-shot acknowledgement
738 * of the gesture rather than progress through a long task. The `0.6s`
739 * duration is short enough to never block the user but long enough
740 * that the deceleration is legible.
741 */
742 .desktop-mode-window__btn--reload.desktop-mode-window__btn--spinning {
743 animation: wpd-reload-spin-once 0.6s cubic-bezier( 0.05, 0.7, 0.1, 1 );
744 }
745
746 @keyframes wpd-reload-spin-once {
747 from {
748 transform: rotate( 0deg );
749 }
750 to {
751 transform: rotate( 360deg );
752 }
753 }
754
755 @media ( prefers-reduced-motion: reduce ) {
756 .desktop-mode-window__btn--reload.desktop-mode-window__btn--spinning {
757 animation: none;
758 }
759 }
760