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

752 lines 25.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 }
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
349 /* Focused window: meta buttons visible. */
350 .desktop-mode-window--focused .desktop-mode-window__meta-btn {
351 color: rgba(255, 255, 255, 0.65);
352 }
353 .desktop-mode-window--focused .desktop-mode-window__meta-btn:hover {
354 color: #fff;
355 background: rgba(255, 255, 255, 0.18);
356 }
357 .desktop-mode-window--focused .desktop-mode-window__meta-btn:focus-visible {
358 color: #fff;
359 background: rgba(255, 255, 255, 0.18);
360 outline: 2px solid rgba(255, 255, 255, 0.6);
361 outline-offset: 1px;
362 }
363 .desktop-mode-window--focused .desktop-mode-window__meta-btn--active {
364 color: #fff;
365 background: rgba(255, 255, 255, 0.25);
366 }
367
368 /* Unfocused window: divider and buttons adapt to light title bar. */
369 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__screen-meta {
370 border-inline-end-color: rgba(0, 0, 0, 0.1);
371 }
372 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn {
373 color: rgba(0, 0, 0, 0.3);
374 }
375 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn:hover {
376 color: rgba(0, 0, 0, 0.6);
377 background: rgba(0, 0, 0, 0.08);
378 }
379 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn--active {
380 color: rgba(0, 0, 0, 0.6);
381 background: rgba(0, 0, 0, 0.12);
382 }
383
384 /* ---------------------------------------------------------------
385 * Tab strip — submenu navigation rendered in the parent shell, just
386 * below the title bar. Each tab swaps the iframe URL in place; no
387 * new window opens. Horizontally scrollable on narrow windows so the
388 * same component works on tablet and mobile shells unchanged.
389 * --------------------------------------------------------------- */
390 .desktop-mode-window__tabs {
391 display: flex;
392 flex-shrink: 0;
393 gap: 2px;
394 padding: 0 8px;
395 background: var(--desktop-mode-tabs-bg, #f6f7f7);
396 border-bottom: 1px solid var(--desktop-mode-window-border);
397 overflow-x: auto;
398 overflow-y: hidden;
399 scrollbar-width: thin;
400 /*
401 * Soft edge fades so overflowing tabs tell the user "scroll for
402 * more." The mask is always applied — when the strip doesn't
403 * overflow, the faded ends are empty area beyond the last tab so
404 * the cosmetic is invisible. When it DOES overflow, the last-
405 * visible tab fades under the mask, creating the affordance.
406 * `overscroll-behavior-x: contain` keeps horizontal trackpad
407 * swipes inside the strip instead of triggering browser back.
408 */
409 mask-image: linear-gradient(
410 to right,
411 transparent 0,
412 #000 16px,
413 #000 calc(100% - 16px),
414 transparent 100%
415 );
416 -webkit-mask-image: linear-gradient(
417 to right,
418 transparent 0,
419 #000 16px,
420 #000 calc(100% - 16px),
421 transparent 100%
422 );
423 overscroll-behavior-x: contain;
424 }
425
426 /*
427 * When JS assigns the --has-overflow marker (future enhancement), the
428 * mask is stronger. For now the CSS-only mask is subtle enough to be
429 * invisible on non-overflowing strips — mask cut-offs land outside the
430 * flex content.
431 */
432
433 .desktop-mode-window__tab {
434 flex-shrink: 0;
435 display: inline-flex;
436 align-items: center;
437 height: 32px;
438 padding: 0 12px;
439 border: none;
440 background: transparent;
441 color: var(--desktop-mode-tabs-color, #50575e);
442 font: inherit;
443 font-size: 12px;
444 line-height: 1;
445 cursor: pointer;
446 border-bottom: 2px solid transparent;
447 margin-bottom: -1px;
448 white-space: nowrap;
449 transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
450 }
451
452 .desktop-mode-window__tab:hover {
453 color: var(--wp-admin-theme-color, #2271b1);
454 background: rgba(0, 0, 0, 0.03);
455 }
456
457 .desktop-mode-window__tab:focus-visible {
458 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
459 outline-offset: -2px;
460 }
461
462 .desktop-mode-window__tab--active {
463 color: var(--wp-admin-theme-color, #2271b1);
464 border-bottom-color: var(--wp-admin-theme-color, #2271b1);
465 font-weight: 600;
466 }
467
468 /*
469 * External sub-tab. Same shape as a submenu tab, plus two inline
470 * chips: detach (↗) and close (×). Chips are painted as spans
471 * (rather than nested buttons — nested interactive elements are a
472 * pain for a11y). The tab's click handler routes chip clicks via
473 * the `data-tab-action` dataset attribute.
474 */
475 .desktop-mode-window__tab--external {
476 /* Slightly wider gap between the label and the chip cluster, and
477 * more breathing room at the trailing edge so the chips aren't
478 * flush against the tab boundary. */
479 gap: 10px;
480 padding-inline-end: 4px;
481 }
482
483 .desktop-mode-window__tab-label {
484 max-width: 180px;
485 overflow: hidden;
486 text-overflow: ellipsis;
487 white-space: nowrap;
488 }
489
490 /* External-tab action chips moved to `<wpd-tab-chip>`. Spacing
491 * between chips handled by a single host-level rule since the
492 * component doesn't know about its siblings. */
493 wpd-tab-chip + wpd-tab-chip {
494 margin-inline-start: 4px;
495 }
496
497 /* Window body: contains the iframe. */
498 .desktop-mode-window__body {
499 flex: 1;
500 position: relative;
501 overflow: hidden;
502 }
503
504 /* Iframe fills the window body. */
505 .desktop-mode-window__iframe {
506 width: 100%;
507 height: 100%;
508 border: none;
509 display: block;
510 background: var(--desktop-mode-window-bg);
511 opacity: 1;
512 transition: opacity 0.25s ease;
513 }
514
515 /* Cross-window drag drop-overlay rules live in windows.css, NOT here.
516 * Putting them in this @import'd sub-sheet means browsers can serve a
517 * stale cached copy even when the parent windows.css has been cache-
518 * busted by mtime, leaving the drag stuck at the iframe boundary
519 * until the user manually clears their stylesheet cache. Keep them
520 * co-located with the @import declarations themselves. */
521
522 /*
523 * Loading-state overlay — painted by `src/window/dom.ts` into
524 * every window's body and removed once the body's content reports
525 * ready. The `<wpd-spinner>` inside is sized responsively against
526 * the window's width via `clamp(96px, 14vw, 192px)` so a tiny
527 * popover gets a small spinner and a maximized window gets a big
528 * one. Placed above the body content (iframe / native render
529 * output) and kept aria-hidden so the spinner's own SR-label is
530 * the only loading announcement.
531 *
532 * `transition-delay` on the overlay's fade-in means a render
533 * that lands within ~120ms never paints the spinner — the
534 * overlay only becomes visible when there's actually a wait
535 * worth surfacing. This keeps the affordance from flashing on
536 * fast local-dev loads while still covering the slow production
537 * iframe boots that motivated it.
538 */
539 .desktop-mode-window__loading {
540 position: absolute;
541 inset: 0;
542 display: flex;
543 align-items: center;
544 justify-content: center;
545 pointer-events: none;
546 background: var(--desktop-mode-window-bg);
547 opacity: 0;
548 /* Default (loaded → loading-removed) — fade out immediately. */
549 transition: opacity 0.25s ease;
550 z-index: 1;
551 }
552
553 .desktop-mode-window__body--loading > .desktop-mode-window__iframe,
554 .desktop-mode-window__body--loading > :not(.desktop-mode-window__loading) {
555 opacity: 0;
556 transition: opacity 0.25s ease;
557 }
558
559 .desktop-mode-window__body--loading > .desktop-mode-window__loading {
560 opacity: 1;
561 /* Entry transition with a small delay — loads that finish in
562 * under ~120ms never paint the spinner, so fast local-dev /
563 * hot-cache fires don't flash. Slow production iframe boots
564 * still see the spinner. */
565 transition-delay: 0.12s;
566 }
567
568 @media ( prefers-reduced-motion: reduce ) {
569 .desktop-mode-window__iframe,
570 .desktop-mode-window__loading,
571 .desktop-mode-window__body--loading > .desktop-mode-window__iframe,
572 .desktop-mode-window__body--loading > :not(.desktop-mode-window__loading) {
573 transition: none;
574 }
575 }
576
577 /*
578 * External sub-tab iframes stack in the same body as the primary
579 * iframe. Only one is visible at a time (managed by `switchToTab`
580 * via `style.display`). Absolute positioning so they don't push the
581 * primary iframe around when added to the DOM; `display: none` on
582 * inactive iframes is set inline by the JS.
583 */
584 .desktop-mode-window__iframe--external {
585 position: absolute;
586 inset: 0;
587 }
588
589 /*
590 * Native window body — fills the window but lets its contents scroll
591 * independently. Iframe bodies use `overflow: hidden` so the iframe
592 * controls its own scroll; native bodies render real document content
593 * that needs the parent to manage overflow.
594 */
595 .desktop-mode-window__body--native {
596 overflow: auto;
597 color: #1d2327;
598 background: var(--desktop-mode-window-bg);
599 }
600
601 /*
602 * Highlight rings. Toggled from JS by `Window.setHighlight()` —
603 * used by plugins that need to point at a window from outside it
604 * (e.g. a "connect to" dropdown that previews candidate windows on
605 * hover). `--wp-window-highlight-color` is overridable per-instance
606 * via `setHighlight( mode, { color } )` or globally via the
607 * variable.
608 */
609 .wp-window {
610 --wp-window-highlight-color: var(
611 --wp-admin-theme-color, #2271b1
612 );
613 }
614 .wp-window--highlight-preview {
615 box-shadow: 0 0 0 3px var( --wp-window-highlight-color ),
616 0 0 24px 4px var( --wp-window-highlight-color );
617 transition: box-shadow 0.12s ease;
618 z-index: 9999;
619 }
620 .wp-window--highlight-persistent {
621 box-shadow: 0 0 0 2px var( --wp-window-highlight-color );
622 transition: box-shadow 0.12s ease;
623 }
624
625 /*
626 * Slots for plugin-registered title-bar buttons. Empty by default
627 * — `display: contents` hides them entirely from layout when no
628 * plugin has registered a button for the window. When buttons ARE
629 * present, they sit inline next to the title (left slot) or just
630 * before the window controls (right slot).
631 */
632 .desktop-mode-window__custom-buttons {
633 display: contents;
634 }
635 .desktop-mode-window__btn--custom {
636 margin: 0 2px;
637 }
638 /*
639 * Plugin-supplied icons render in the host's light DOM so the
640 * global Dashicons stylesheet reaches them (shadow DOM doesn't
641 * inherit page CSS).
642 *
643 * Dashicons render at their native 20×20 — that's the size the
644 * font is hinted for and the only one where glyph metrics put
645 * the visual centre on the geometric centre. Earlier we tried
646 * down-scaling them to 14×14 to match the built-in chrome icons,
647 * but font-size: 14 on a Dashicon shifts the glyph 1–2 pixels
648 * off centre because the font's ascent/descent ratio doesn't
649 * scale linearly. The 30×30 button has 5px of padding around a
650 * 20×20 glyph — comfortable, and visually consistent with the
651 * 14×14 chrome icons because both are flex-centred in the same
652 * box.
653 *
654 * For inline-SVG icons that plugin authors ship without explicit
655 * width/height, we DO clamp to 18×18 — those are the cases where
656 * a bare `<svg>` would otherwise size to its viewBox or default
657 * to 300×150 (the spec default) and overflow the button.
658 */
659 .desktop-mode-window__btn--custom svg:not( [ width ] ):not( [ height ] ) {
660 width: 18px;
661 height: 18px;
662 display: block;
663 }
664
665 /* ----------------------------------------------------------------
666 * Custom-chrome marker — hides every framework-shipped titlebar
667 * child while a plugin's chrome is mounted.
668 *
669 * The window gets the `desktop-mode-window--custom-chrome` class the
670 * moment `mountWindowChrome` succeeds (BEFORE the plugin's
671 * `render()` runs). Default children carry the
672 * `data-desktop-mode-default-chrome` attribute stamped at element-
673 * creation time. The combination is a load-bearing guarantee:
674 * even if the plugin's render() doesn't clear `titlebar.innerHTML`,
675 * even if a plugin's destroy() leaks the standard chrome back into
676 * place, even mid-fade during a window close — the default chrome
677 * NEVER becomes visible while the marker class is active.
678 *
679 * The class is removed only when the chrome is swapped to standard
680 * (in `Window.remountWindowChrome`); during a window close it
681 * persists until `element.remove()` runs in `onDone()`, which is
682 * after the fade has reached opacity 0 — so the user never sees
683 * the swap.
684 *
685 * @since 0.18.0
686 * ---------------------------------------------------------------- */
687
688 .desktop-mode-window--custom-chrome
689 > .desktop-mode-window__titlebar
690 > [ data-desktop-mode-default-chrome ] {
691 display: none !important;
692 }
693
694 /* ---------------------------------------------------------------
695 * Reload button feedback.
696 *
697 * Click feedback is decoupled from the loading state:
698 *
699 * 1. On click, the icon performs a single 360° rotation with a
700 * fast-start / slow-end ease curve — confirms the user's
701 * gesture independently of how long the page takes to load.
702 * The `--spinning` class is added by the click handler and
703 * removed on `animationend` so a subsequent click restarts
704 * the animation cleanly.
705 *
706 * 2. While the window body is in the `--loading` state (set by
707 * `markContentLoading()` and cleared on the iframe's
708 * `desktop-mode-ready` postMessage), the button is dimmed and
709 * non-interactive so a second click can't desync the
710 * chromeless bridge's loading handshake.
711 *
712 * `:has()` is used instead of mirroring the `--loading` modifier
713 * onto the window root because upstream's loading API only
714 * decorates the body element. Browsers without :has() (very old)
715 * fall back to a static dimmed button — feature, not bug.
716 * --------------------------------------------------------------- */
717
718 .desktop-mode-window:has( .desktop-mode-window__body--loading )
719 .desktop-mode-window__btn--reload {
720 pointer-events: none;
721 opacity: 0.55;
722 }
723
724 /*
725 * Click feedback animation. The custom easing — `cubic-bezier( 0.05,
726 * 0.7, 0.1, 1 )` — is a steeper-than-default ease-out: ~70% of the
727 * rotation lands in the first 30% of the duration, then it decelerates.
728 * Reads as "snap, then settle" rather than the linear glide of a
729 * loading-spinner, reinforcing that this is a one-shot acknowledgement
730 * of the gesture rather than progress through a long task. The `0.6s`
731 * duration is short enough to never block the user but long enough
732 * that the deceleration is legible.
733 */
734 .desktop-mode-window__btn--reload.desktop-mode-window__btn--spinning {
735 animation: wpd-reload-spin-once 0.6s cubic-bezier( 0.05, 0.7, 0.1, 1 );
736 }
737
738 @keyframes wpd-reload-spin-once {
739 from {
740 transform: rotate( 0deg );
741 }
742 to {
743 transform: rotate( 360deg );
744 }
745 }
746
747 @media ( prefers-reduced-motion: reduce ) {
748 .desktop-mode-window__btn--reload.desktop-mode-window__btn--spinning {
749 animation: none;
750 }
751 }
752