PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.6
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.6, at assets/css/window-chrome.css

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