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

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

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