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 / desktop.css

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

1,157 lines 31.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Desktop Mode — Shell Layout.
3 *
4 * When desktop mode is active, the classic admin chrome still renders
5 * server-side (for hook compatibility) but is hidden with CSS. The
6 * plugin-injected shell floats above it, positioned just below the
7 * fixed admin bar, covering the rest of the viewport.
8 *
9 * @since 0.1.0
10 */
11
12 /* Solid backdrop behind the shell. */
13 body.desktop-mode-active {
14 background: #1d2327;
15 overflow: hidden;
16 }
17
18 /*
19 * Cursor policy — default arrow everywhere in desktop mode.
20 *
21 * Per user preference (since 0.20.0): no hand/pointer/grab/grabbing/
22 * copy/no-drop cursors on icons, tiles, dock items, dock peek,
23 * window chrome, or any other clickable surface. The default arrow
24 * is the only "neutral" cursor we render.
25 *
26 * Exceptions:
27 * - Window + widget resize handles keep their direction-specific
28 * cursors (ns-resize, ew-resize, nesw-resize, nwse-resize) —
29 * they're a functional cue, not a hover affordance, and removing
30 * them would make corner-grabs guesswork.
31 * - Text inputs / textareas / contenteditable surfaces keep the
32 * browser-default I-beam.
33 *
34 * `!important` is needed because the specific `cursor: pointer` and
35 * drag-state declarations elsewhere in the codebase have the same or
36 * higher specificity. The resize-handle exceptions use `!important`
37 * for the same reason.
38 *
39 * Shadow-DOM web components (`wpd-*`) define their own cursors
40 * inside their shadow trees; those internal rules win regardless of
41 * what we set on the host. Anything visible on the desktop shell
42 * (light DOM) is covered by this policy.
43 */
44 body.desktop-mode-active,
45 body.desktop-mode-active * {
46 cursor: default !important;
47 }
48
49 body.desktop-mode-active input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]),
50 body.desktop-mode-active textarea,
51 body.desktop-mode-active [contenteditable=""],
52 body.desktop-mode-active [contenteditable="true"] {
53 cursor: text !important;
54 }
55
56 body.desktop-mode-active .desktop-mode-window__resize-handle--se,
57 body.desktop-mode-active .desktop-mode-window__resize-handle--nw,
58 body.desktop-mode-active .desktop-mode-widgets__resize--se,
59 body.desktop-mode-active .desktop-mode-widgets__resize--nw {
60 cursor: nwse-resize !important;
61 }
62
63 body.desktop-mode-active .desktop-mode-window__resize-handle--sw,
64 body.desktop-mode-active .desktop-mode-window__resize-handle--ne,
65 body.desktop-mode-active .desktop-mode-widgets__resize--sw,
66 body.desktop-mode-active .desktop-mode-widgets__resize--ne {
67 cursor: nesw-resize !important;
68 }
69
70 body.desktop-mode-active .desktop-mode-widgets__resize--n,
71 body.desktop-mode-active .desktop-mode-widgets__resize--s {
72 cursor: ns-resize !important;
73 }
74
75 body.desktop-mode-active .desktop-mode-widgets__resize--e,
76 body.desktop-mode-active .desktop-mode-widgets__resize--w {
77 cursor: ew-resize !important;
78 }
79
80 /*
81 * Hide the classic admin chrome while the shell is active.
82 *
83 * The markup still exists in the DOM so server-side action hooks that
84 * target these regions (admin_notices, in_admin_footer, etc.) continue
85 * to fire — we just take them out of the visual flow.
86 */
87 body.desktop-mode-active #adminmenuwrap,
88 body.desktop-mode-active #adminmenuback,
89 body.desktop-mode-active #wpbody,
90 body.desktop-mode-active #wpfooter,
91 body.desktop-mode-active .wp-responsive-toggle,
92 body.desktop-mode-active #collapse-menu {
93 display: none !important;
94 }
95
96 /*
97 * The admin bar is the ONLY escape hatch in desktop mode — it hosts the
98 * "Switch to Classic Admin" toggle. Any plugin CSS, Gutenberg state,
99 * user preference, or stray `display:none` that hides it would strand
100 * the user inside the shell with no way out. Hard-pin it visible.
101 *
102 * The exception: while a window is in immersive fullscreen, we *want*
103 * the bar hidden (macOS convention). That rule lives below and wins on
104 * specificity because it also targets #wpadminbar under a body class.
105 */
106 body.desktop-mode-active #wpadminbar {
107 display: block !important;
108 visibility: visible !important;
109 opacity: 1 !important;
110 position: fixed !important;
111 inset-block-start: 0 !important;
112 inset-inline-start: 0 !important;
113 inset-inline-end: 0 !important;
114 }
115
116 /*
117 * Align the admin-bar W logo with the dock below it.
118 *
119 * Without this rule the admin bar's leftmost item (the WordPress W)
120 * renders at its Core-dictated ~44 px width, while the dock below it
121 * can be 48, 56, or 72 px wide depending on the user's OS-Settings
122 * preference. The misalignment reads as "two disconnected left
123 * regions" — visually scruffy on an otherwise tight shell.
124 *
125 * We size the logo's <li> to match the dock's width and center the
126 * W inside, so the two columns share an axis. `--desktop-mode-dock-width`
127 * is now set on :root (see settings.ts → apply()) so it reaches
128 * siblings of #desktop-mode-shell like the admin bar.
129 */
130 body.desktop-mode-active #wpadminbar #wp-admin-bar-wp-logo {
131 width: var( --desktop-mode-dock-width, 56px );
132 }
133
134 body.desktop-mode-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item {
135 display: flex !important;
136 align-items: center;
137 justify-content: center;
138 padding-inline: 0 !important;
139 }
140
141 /*
142 * The W glyph is painted via a ::before pseudo on `.ab-icon`. Center
143 * it within the flex cell so varying dock widths don't leave it
144 * visually offset.
145 */
146 body.desktop-mode-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item > .ab-icon {
147 margin: 0;
148 }
149
150 /*
151 * Classic wpcontent carries a `margin-left: 160px` for the sidebar. With
152 * the sidebar hidden we let wpcontent fall back to flush — the shell is
153 * fixed-positioned anyway so this doesn't change the shell's layout, it
154 * just prevents a phantom sidebar gutter in any stray visible content.
155 */
156 body.desktop-mode-active #wpcontent {
157 margin-inline-start: 0;
158 padding-inline-start: 0;
159 }
160
161 /*
162 * Desktop shell — fills the viewport below the admin bar.
163 * Uses position: fixed so it's unaffected by any containing-block
164 * quirks in #wpwrap/#wpcontent.
165 */
166 .desktop-mode-shell {
167 position: fixed;
168 inset-block-start: var(--wp-admin--admin-bar--height, 32px);
169 inset-inline-start: 0;
170 inset-inline-end: 0;
171 inset-block-end: 0;
172 display: flex;
173 flex-direction: column;
174 overflow: hidden;
175 z-index: var(--desktop-mode-z-base);
176 }
177
178 /*
179 * Wallpaper layer — first child of the shell, sits behind the dock
180 * and desktop area so a translucent dock shows through to the
181 * background (macOS pattern). CSS wallpapers paint via the
182 * `--desktop-mode-bg` custom property; canvas wallpapers mount their
183 * own DOM (typically a <canvas>) into this element via JS.
184 *
185 * z-index: 0 keeps it below the shell body flex row which has no
186 * explicit z-index of its own but establishes its own local
187 * stacking context via `position: relative`, so the dock + windows
188 * always render above the wallpaper regardless of DOM order.
189 */
190 .desktop-mode-wallpaper {
191 position: absolute;
192 inset: 0;
193 z-index: 0;
194 background: var(--desktop-mode-bg);
195 overflow: hidden;
196 pointer-events: none;
197 }
198
199 /*
200 * Canvas children fill the layer and ignore pointer events by
201 * default — a wallpaper is presentational chrome, not an interactive
202 * element. Plugins that genuinely need a clickable wallpaper can
203 * override `pointer-events` on their own mounted node.
204 */
205 .desktop-mode-wallpaper > canvas,
206 .desktop-mode-wallpaper > * {
207 width: 100%;
208 height: 100%;
209 display: block;
210 }
211
212 /* Below 783px WP collapses the admin bar to 46px. */
213 @media screen and (max-width: 782px) {
214 .desktop-mode-shell {
215 inset-block-start: var(--wp-admin--admin-bar--height, 46px);
216 }
217 }
218
219 /*
220 * Immersive fullscreen: when any window is in fullscreen state, hide the
221 * admin bar and let the shell cover the full viewport. This keeps the
222 * fullscreen window visually above all chrome without fighting the shell's
223 * stacking context (a fullscreen window lives inside the shell, so its
224 * z-index is always bounded by the shell's — moving it out of the shell
225 * to raise it would break focus, drag, and session snapshotting).
226 *
227 * The window's title bar remains visible because the window itself is
228 * position: fixed at top: 0, so the focus/exit-fullscreen button is
229 * still reachable.
230 */
231 body.desktop-mode-has-fullscreen-window #wpadminbar {
232 display: none !important;
233 }
234
235 body.desktop-mode-has-fullscreen-window .desktop-mode-shell {
236 inset-block-start: 0;
237 }
238
239 /* Shell body: contains dock + desktop area side by side. */
240 .desktop-mode-shell__body {
241 flex: 1;
242 display: flex;
243 overflow: hidden;
244 position: relative;
245 }
246
247 /*
248 * Desktop area — where windows float. Background is now transparent
249 * because the wallpaper renders in the shell-level layer behind
250 * both this area and the dock. Stacking context via position: relative
251 * ensures children (windows) sit above the wallpaper.
252 *
253 * `padding-bottom` reserves room for the floating bottom dock pill
254 * that every layout (Classic, Unified, Spatial) puts at the bottom
255 * — 40px icons + 16px padding + 12px gap below + 8px breathing room.
256 */
257 .desktop-mode-area {
258 flex: 1;
259 position: relative;
260 overflow: hidden;
261 background: transparent;
262 padding-bottom: 80px;
263 transition: opacity 180ms ease;
264 }
265
266 /*
267 * Boot reveal gate.
268 *
269 * On F5 the shell paints in stages: the dispatcher's
270 * `repaintIcons()` lands the server-rendered wallpaper icons
271 * synchronously from `desktopModeConfig.desktopIcons`, then the
272 * files-layer mounts and momentarily renders an empty bucket while
273 * REST `listPlacements(0)` is in flight, then folders/posts/links
274 * slot in on the next paint. The user reads that as "plugins show
275 * first, then everything blinks and the rest arrives."
276 *
277 * The `--booting` modifier is applied BY PHP (see
278 * `includes/render/shell.php`) on the initial HTML so the area
279 * starts invisible BEFORE any JS runs — otherwise the dispatcher
280 * gets to paint the wallpaper icons into a visible area first and
281 * the user sees them flash before our gate kicks in.
282 *
283 * `desktop.ts` removes the class inside a `requestAnimationFrame`
284 * after the root files-layer's `hydrated` promise resolves (with a
285 * 2 s JS safety timeout). The opacity transition declared on
286 * `.desktop-mode-area` smooths the reveal.
287 *
288 * The fallback animation is a resilience layer for the case where
289 * the shell JS never runs (network error mid-load, ad-blocker
290 * tearing down a bundle, …). The animation holds the area
291 * invisible for 2.7 s then flips it to opacity 1; the
292 * `animation-fill-mode: forwards` keeps the revealed state held
293 * after the animation ends. In the normal case JS removes the
294 * class long before 2.7 s and the animation is discarded.
295 *
296 * `pointer-events: none` while booting prevents a stray click on
297 * the still-hidden surface from triggering Show-Desktop or opening
298 * a tile that's about to reposition once REST returns.
299 *
300 * @since 0.18.x
301 */
302 .desktop-mode-area--booting {
303 opacity: 0;
304 pointer-events: none;
305 animation: desktop-mode-area-boot-fallback 3s forwards;
306 }
307
308 @keyframes desktop-mode-area-boot-fallback {
309 0%,
310 90% {
311 opacity: 0;
312 pointer-events: none;
313 }
314 100% {
315 opacity: 1;
316 pointer-events: auto;
317 }
318 }
319
320 /* ---------------------------------------------------------------
321 * Desktop icons — wallpaper shortcut tiles registered via the
322 * `wp_register_desktop_icon()` PHP API. Rendered inside the
323 * desktop area as the first child after the wallpaper, so they
324 * paint above the wallpaper but beneath any window.
325 *
326 * Grid layout is fixed-width columns; tiles wrap onto multiple
327 * rows when the desktop is wide enough to host more than one
328 * column's worth. `pointer-events: none` on the container keeps
329 * the background clickable (for "minimize all" on wallpaper
330 * click) while individual tiles re-enable pointer events so they
331 * remain clickable.
332 * --------------------------------------------------------------- */
333
334 .desktop-mode-icons {
335 position: absolute;
336 inset-block-start: 16px;
337 inset-block-end: 16px;
338 inset-inline-start: 16px;
339 inset-inline-end: 16px;
340 display: grid;
341 /*
342 * Column-major flow: icons fill the first column top-to-bottom,
343 * then start the next column. Matches the macOS / Finder
344 * convention. `grid-auto-flow: column` plus `auto-fill` rows
345 * (sized to the container height) yields exactly that layout.
346 *
347 * All four insets are pinned so the container has a definite
348 * height — `repeat(auto-fill, 96px)` needs that to compute the
349 * row count. 96px is one icon button (48px image + 6px gap +
350 * up-to-two-line label + 8px vertical padding); a slightly
351 * generous figure leaves room for descenders without clipping.
352 */
353 grid-auto-flow: column;
354 grid-template-rows: repeat(auto-fill, 96px);
355 grid-auto-columns: 88px;
356 align-content: start;
357 justify-content: start;
358 gap: 12px;
359 z-index: 1;
360 pointer-events: none;
361 /*
362 * Spill protection: if a user ever pins more icons than fit
363 * vertically, the grid scrolls horizontally so the unreachable
364 * tiles can be reached without clipping the visible ones.
365 * Individual tiles re-enable pointer events so they stay
366 * clickable inside the scroll surface.
367 */
368 overflow: auto;
369 }
370
371 .desktop-mode-icon {
372 pointer-events: auto;
373 display: flex;
374 flex-direction: column;
375 align-items: center;
376 justify-content: flex-start;
377 gap: 6px;
378 width: 88px;
379 padding: 8px 4px;
380 border: 0;
381 background: transparent;
382 color: var(--desktop-mode-fg, #fff);
383 cursor: pointer;
384 border-radius: 8px;
385 transition: background-color 0.15s ease;
386 /* Positioning context for `.desktop-mode-icon__badge`. */
387 position: relative;
388 }
389
390 /*
391 * Icon badge — symmetric to `.desktop-mode-dock__badge` but anchored
392 * to the wallpaper-icon image. Same gradient, same ring, slightly
393 * larger so it reads at the longer wallpaper viewing distance.
394 * Painted by `wp.desktop.icons.setBadge( id, count )` — see
395 * `src/desktop-icons.ts` for the imperative API and
396 * `src/recycle-bin/badge.ts` for the canonical multi-rail
397 * consumer. The class name is part of the stable contract; the
398 * positioning, color, and shadow are not — themes or future
399 * shell builds may restyle freely.
400 */
401 .desktop-mode-icon__badge {
402 position: absolute;
403 top: 4px;
404 inset-inline-end: 14px;
405 min-width: 18px;
406 height: 18px;
407 padding: 0 5px;
408 box-sizing: border-box;
409 border-radius: 999px;
410 background: linear-gradient( 180deg, #ff5a5a 0%, #d63638 100% );
411 color: #fff;
412 display: inline-flex;
413 align-items: center;
414 justify-content: center;
415 font-size: 11px;
416 font-weight: 700;
417 line-height: 1;
418 font-variant-numeric: tabular-nums;
419 box-shadow:
420 0 0 0 1.5px rgba( 0, 0, 0, 0.4 ),
421 0 1px 3px rgba( 0, 0, 0, 0.35 );
422 pointer-events: none;
423 }
424
425 .desktop-mode-icon:hover,
426 .desktop-mode-icon:focus-visible {
427 background: rgba(255, 255, 255, 0.12);
428 outline: none;
429 }
430
431 .desktop-mode-icon:focus-visible {
432 box-shadow: 0 0 0 2px var(--wp-admin-theme-color, #2271b1);
433 }
434
435 .desktop-mode-icon__image {
436 display: inline-flex;
437 align-items: center;
438 justify-content: center;
439 width: 48px;
440 height: 48px;
441 font-size: 32px;
442 line-height: 1;
443 }
444
445 .desktop-mode-icon__image img {
446 max-width: 48px;
447 max-height: 48px;
448 object-fit: contain;
449 }
450
451 .desktop-mode-icon__label {
452 font-size: 12px;
453 text-align: center;
454 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
455 line-height: 1.2;
456 word-break: break-word;
457 }
458
459 /* ---------------------------------------------------------------
460 * Sticky notes — Gutenberg `wp_guideline` artifacts tagged with the
461 * sticky guideline type. Notes sit above wallpaper icons/widgets and
462 * below windows, so they behave like desktop objects rather than app
463 * chrome.
464 * --------------------------------------------------------------- */
465
466 .desktop-mode-sticky-notes {
467 position: absolute;
468 inset: 0;
469 z-index: 2;
470 pointer-events: none;
471 transition:
472 opacity 0.22s ease,
473 transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
474 }
475
476 .desktop-mode-area--overview .desktop-mode-sticky-notes {
477 opacity: 0;
478 transform: translateY( 12px );
479 pointer-events: none;
480 }
481
482 .desktop-mode-sticky-note {
483 position: absolute;
484 display: flex;
485 flex-direction: column;
486 pointer-events: auto;
487 background: #fff09a;
488 color: #2a2513;
489 border: 1px solid rgba( 84, 67, 0, 0.24 );
490 border-radius: 4px;
491 box-shadow:
492 0 12px 28px rgba( 0, 0, 0, 0.22 ),
493 0 2px 5px rgba( 0, 0, 0, 0.16 ),
494 inset 0 1px 0 rgba( 255, 255, 255, 0.55 );
495 overflow: hidden;
496 resize: both;
497 }
498
499 .desktop-mode-sticky-note--dragging {
500 opacity: 0.92;
501 resize: none;
502 user-select: none;
503 }
504
505 .desktop-mode-sticky-note__header {
506 display: flex;
507 align-items: center;
508 gap: 5px;
509 min-height: 30px;
510 padding: 4px 6px 4px 8px;
511 background: rgba( 82, 64, 0, 0.06 );
512 border-bottom: 1px solid rgba( 82, 64, 0, 0.14 );
513 cursor: grab;
514 touch-action: none;
515 user-select: none;
516 }
517
518 .desktop-mode-sticky-note--dragging .desktop-mode-sticky-note__header {
519 cursor: grabbing;
520 }
521
522 .desktop-mode-sticky-note__grip {
523 flex: 0 0 auto;
524 width: 8px;
525 height: 14px;
526 background-image: radial-gradient(
527 circle,
528 rgba( 60, 48, 0, 0.5 ) 1.2px,
529 transparent 1.5px
530 );
531 background-size: 4px 4px;
532 background-position: 0 1px;
533 background-repeat: space;
534 opacity: 0.42;
535 }
536
537 .desktop-mode-sticky-note__title {
538 flex: 1;
539 min-width: 0;
540 font-size: 11px;
541 font-weight: 650;
542 overflow: hidden;
543 text-overflow: ellipsis;
544 white-space: nowrap;
545 }
546
547 .desktop-mode-sticky-note__status {
548 flex: 0 0 auto;
549 }
550
551 .desktop-mode-sticky-note wpd-window-button {
552 --wpd-btn-color: rgba( 42, 37, 19, 0.7 );
553 --wpd-btn-color-hover: #2a2513;
554 --wpd-btn-bg-hover: rgba( 42, 37, 19, 0.1 );
555 --wpd-btn-bg-active: rgba( 42, 37, 19, 0.16 );
556 --wpd-btn-danger-hover: rgba( 214, 54, 56, 0.18 );
557 --wpd-btn-outline: var( --wp-admin-theme-color, #2271b1 );
558 }
559
560 .desktop-mode-sticky-note__open.is-disabled {
561 opacity: 0.34;
562 pointer-events: none;
563 }
564
565 .desktop-mode-sticky-note__editor {
566 flex: 1;
567 min-height: 0;
568 --desktop-mode-window-bg: transparent;
569 --desktop-mode-border: transparent;
570 --desktop-mode-text: #2a2513;
571 --desktop-mode-muted: rgba( 42, 37, 19, 0.62 );
572 font-family:
573 -apple-system,
574 BlinkMacSystemFont,
575 "Segoe UI",
576 sans-serif;
577 }
578
579 .desktop-mode-sticky-note__editor::part(textarea) {
580 height: 100%;
581 min-height: 100%;
582 padding: 10px 12px 12px;
583 background: transparent;
584 border: 0;
585 border-radius: 0;
586 box-shadow: none;
587 color: #2a2513;
588 font-size: 13px;
589 line-height: 1.4;
590 resize: none;
591 }
592
593 .desktop-mode-sticky-note__editor::part(textarea):hover,
594 .desktop-mode-sticky-note__editor::part(textarea):focus-visible {
595 border-color: transparent;
596 box-shadow: none;
597 }
598
599 /* ---------------------------------------------------------------
600 * Widgets column — right-edge glass strip that paints above the
601 * wallpaper but beneath windows. Hosts stacked widget cards and a
602 * trailing "Add widget" tile. Interactive only on its own cards +
603 * the add tile; every other pixel is pointer-transparent so window
604 * drag / resize / click-through behaviour is unaffected.
605 * --------------------------------------------------------------- */
606
607 .desktop-mode-widgets {
608 position: absolute;
609 top: 16px;
610 bottom: 16px;
611 inset-inline-end: 16px;
612 width: 320px;
613 display: flex;
614 flex-direction: column;
615 gap: 12px;
616 z-index: 1;
617 pointer-events: none;
618 overflow-y: auto;
619 /* Hide the scrollbar chrome unless the user actually overflows —
620 * most setups never will. */
621 scrollbar-width: thin;
622 /* Fade alongside the overview backdrop. The class flip that
623 * drives this is on the parent area, set at t=0 of enter and
624 * removed at t=0 of exit, so widgets fade in parallel with the
625 * dock collapse / return — never sequential. */
626 opacity: 1;
627 transition:
628 opacity 0.22s ease,
629 transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
630 }
631
632 .desktop-mode-area--overview .desktop-mode-widgets {
633 opacity: 0;
634 /* Slight slide-off-right so the fade reads as "the column is
635 * stepping back out of the way" rather than flat-dimming. */
636 transform: translateX( 16px );
637 pointer-events: none;
638 }
639
640 .desktop-mode-widgets__list {
641 display: flex;
642 flex-direction: column;
643 gap: 12px;
644 }
645
646 /*
647 * Individual widget card — glass backdrop matching the dock /
648 * overview top bar. Card root is pointer-events: auto so clicks on
649 * content + the remove X land; the column itself stays
650 * pointer-transparent so a drag that grazes the column's margin
651 * falls through to the window beneath.
652 */
653 .desktop-mode-widgets__card {
654 position: relative;
655 padding: 0;
656 pointer-events: auto;
657 background: rgba( 20, 20, 22, 0.55 );
658 backdrop-filter: blur( 18px ) saturate( 140% );
659 -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
660 border: 1px solid rgba( 255, 255, 255, 0.08 );
661 border-radius: 14px;
662 color: #fff;
663 box-shadow:
664 0 8px 28px rgba( 0, 0, 0, 0.35 ),
665 inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
666 transition:
667 transform 0.18s ease,
668 box-shadow 0.18s ease,
669 opacity 0.18s ease;
670 }
671
672 .desktop-mode-widgets__card:hover {
673 transform: translateX( -2px );
674 box-shadow:
675 0 12px 36px rgba( 0, 0, 0, 0.45 ),
676 inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
677 }
678
679 .desktop-mode-widgets__card-body {
680 padding: 16px;
681 min-height: 48px;
682 }
683
684 .desktop-mode-widgets__card-close {
685 position: absolute;
686 top: 6px;
687 inset-inline-end: 6px;
688 width: 22px;
689 height: 22px;
690 display: flex;
691 align-items: center;
692 justify-content: center;
693 padding: 0;
694 border: 0;
695 background: rgba( 0, 0, 0, 0.55 );
696 color: rgba( 255, 255, 255, 0.75 );
697 border-radius: 50%;
698 cursor: pointer;
699 opacity: 0;
700 transition:
701 opacity 0.15s ease,
702 background-color 0.15s ease,
703 color 0.15s ease;
704 }
705
706 .desktop-mode-widgets__card:hover .desktop-mode-widgets__card-close,
707 .desktop-mode-widgets__card-close:focus-visible {
708 opacity: 1;
709 }
710
711 .desktop-mode-widgets__card-close:hover {
712 background: #d63638;
713 color: #fff;
714 }
715
716 /*
717 * Movable widgets — a thin chrome header at the top of the card holds
718 * the drag grip, the widget's own label, and the × button. The whole
719 * header is the drag handle (except for interactive children — those
720 * remain clickable as normal). Non-movable widgets keep today's
721 * corner-× layout unchanged.
722 */
723 .desktop-mode-widgets__chrome {
724 display: flex;
725 align-items: center;
726 gap: 8px;
727 padding: 6px 8px;
728 border-bottom: 1px solid rgba( 255, 255, 255, 0.06 );
729 cursor: grab;
730 user-select: none;
731 -webkit-user-select: none;
732 touch-action: none;
733 }
734
735 .desktop-mode-widgets__card--dragging .desktop-mode-widgets__chrome {
736 cursor: grabbing;
737 }
738
739 .desktop-mode-widgets__grip {
740 flex-shrink: 0;
741 width: 10px;
742 height: 16px;
743 background-image: radial-gradient(
744 circle,
745 rgba( 255, 255, 255, 0.55 ) 1.2px,
746 transparent 1.5px
747 );
748 background-size: 5px 5px;
749 background-position: 0 1px;
750 background-repeat: space;
751 opacity: 0.45;
752 transition: opacity 0.12s ease;
753 }
754
755 .desktop-mode-widgets__card:hover .desktop-mode-widgets__grip {
756 opacity: 0.85;
757 }
758
759 .desktop-mode-widgets__title {
760 flex: 1;
761 font-size: 12px;
762 font-weight: 500;
763 letter-spacing: 0.01em;
764 color: rgba( 255, 255, 255, 0.85 );
765 overflow: hidden;
766 text-overflow: ellipsis;
767 white-space: nowrap;
768 }
769
770 /* When the close button sits inside the chrome it's inline, not
771 corner-absolute — reset position + always-visible opacity. */
772 .desktop-mode-widgets__chrome .desktop-mode-widgets__card-close {
773 position: static;
774 opacity: 0.7;
775 width: 20px;
776 height: 20px;
777 }
778
779 .desktop-mode-widgets__chrome .desktop-mode-widgets__card-close:hover,
780 .desktop-mode-widgets__chrome .desktop-mode-widgets__card-close:focus-visible {
781 opacity: 1;
782 }
783
784 /*
785 * Re-dock button — sits in the chrome next to the close button and
786 * returns a floating widget to the right column. Visibility is
787 * gated on the `--floating` modifier class so docked widgets never
788 * expose it (tapping it on a docked card would be a confusing
789 * no-op). The DOM node stays mounted either way so state flips
790 * don't rebuild chrome.
791 *
792 * Styling mirrors the inline close button: 20×20 round tile,
793 * dimmed by default, full-opacity on hover / focus. Distinct hover
794 * color (theme accent, not red) signals "put back" vs. "remove."
795 */
796 .desktop-mode-widgets__card-redock {
797 display: none;
798 align-items: center;
799 justify-content: center;
800 padding: 0;
801 border: 0;
802 background: rgba( 0, 0, 0, 0.55 );
803 color: rgba( 255, 255, 255, 0.75 );
804 border-radius: 50%;
805 width: 20px;
806 height: 20px;
807 cursor: pointer;
808 opacity: 0.7;
809 transition:
810 opacity 0.15s ease,
811 background-color 0.15s ease,
812 color 0.15s ease;
813 }
814
815 .desktop-mode-widgets__card--floating .desktop-mode-widgets__card-redock {
816 display: flex;
817 }
818
819 .desktop-mode-widgets__card-redock:hover,
820 .desktop-mode-widgets__card-redock:focus-visible {
821 opacity: 1;
822 background: var( --wp-admin-theme-color, #2271b1 );
823 color: #fff;
824 }
825
826 /*
827 * Floating widgets — absolute-positioned, inline top/left/width/height
828 * written by the frame. The hover translate-X effect would fight the
829 * user's chosen position, so disable it.
830 */
831 .desktop-mode-widgets__card--floating {
832 position: absolute;
833 margin: 0;
834 }
835
836 .desktop-mode-widgets__card--floating:hover {
837 transform: none;
838 }
839
840 .desktop-mode-widgets__card--dragging,
841 .desktop-mode-widgets__card--resizing {
842 transition: none;
843 z-index: 2;
844 }
845
846 /*
847 * Resize handles — 8 in all, with the 4 edges as hair-line strips and
848 * the 4 corners as small squares. Invisible by default; cursor change
849 * telegraphs them on hover. The frame attaches pointer listeners to
850 * every handle; a non-movable widget ignores everything except the
851 * `s` (bottom-edge, height-only) handle at the pointer layer, so we
852 * visually hide the mismatched handles via the modifier class.
853 */
854 .desktop-mode-widgets__resize {
855 position: absolute;
856 z-index: 1;
857 }
858
859 .desktop-mode-widgets__resize--n {
860 top: -3px;
861 left: 8px;
862 right: 8px;
863 height: 6px;
864 cursor: ns-resize;
865 }
866
867 .desktop-mode-widgets__resize--s {
868 bottom: -3px;
869 left: 8px;
870 right: 8px;
871 height: 6px;
872 cursor: ns-resize;
873 }
874
875 .desktop-mode-widgets__resize--e {
876 top: 8px;
877 bottom: 8px;
878 right: -3px;
879 width: 6px;
880 cursor: ew-resize;
881 }
882
883 .desktop-mode-widgets__resize--w {
884 top: 8px;
885 bottom: 8px;
886 left: -3px;
887 width: 6px;
888 cursor: ew-resize;
889 }
890
891 .desktop-mode-widgets__resize--ne {
892 top: -4px;
893 right: -4px;
894 width: 12px;
895 height: 12px;
896 cursor: nesw-resize;
897 }
898
899 .desktop-mode-widgets__resize--nw {
900 top: -4px;
901 left: -4px;
902 width: 12px;
903 height: 12px;
904 cursor: nwse-resize;
905 }
906
907 .desktop-mode-widgets__resize--se {
908 bottom: -4px;
909 right: -4px;
910 width: 12px;
911 height: 12px;
912 cursor: nwse-resize;
913 }
914
915 .desktop-mode-widgets__resize--sw {
916 bottom: -4px;
917 left: -4px;
918 width: 12px;
919 height: 12px;
920 cursor: nesw-resize;
921 }
922
923 /*
924 * Non-movable resizable widgets only expose the bottom-edge handle —
925 * width stays locked to the column width. Hide the other seven so the
926 * cursor doesn't lie about what's interactive.
927 */
928 .desktop-mode-widgets__card--resizable:not( .desktop-mode-widgets__card--movable ) .desktop-mode-widgets__resize:not( .desktop-mode-widgets__resize--s ) {
929 display: none;
930 }
931
932 /*
933 * Add-widget tile — matches the "+" tile in the overview top bar.
934 * Always visible at the bottom of the column; promoted to center
935 * when the column is otherwise empty so first-run users see a
936 * friendly target rather than an empty strip.
937 */
938 .desktop-mode-widgets__add {
939 margin-top: auto;
940 padding: 14px 12px;
941 display: flex;
942 align-items: center;
943 justify-content: center;
944 gap: 8px;
945 pointer-events: auto;
946 background: transparent;
947 border: 2px dashed rgba( 255, 255, 255, 0.22 );
948 border-radius: 14px;
949 color: rgba( 255, 255, 255, 0.72 );
950 font: inherit;
951 font-weight: 500;
952 cursor: pointer;
953 transition:
954 border-color 0.15s ease,
955 background-color 0.15s ease,
956 color 0.15s ease;
957 }
958
959 .desktop-mode-widgets__add:hover {
960 border-color: rgba( 255, 255, 255, 0.4 );
961 background: rgba( 255, 255, 255, 0.06 );
962 color: #fff;
963 }
964
965 .desktop-mode-widgets__add:focus-visible {
966 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
967 outline-offset: 2px;
968 }
969
970 .desktop-mode-widgets__add-plus {
971 font-size: 20px;
972 line-height: 1;
973 font-weight: 200;
974 }
975
976 .desktop-mode-widgets__add-label {
977 font-size: 13px;
978 letter-spacing: 0.02em;
979 }
980
981 /* Built-in clock widget */
982 .desktop-mode-widget-clock {
983 text-align: center;
984 /* The time + date are passive glyphs, not content users would
985 * ever reasonably select — turning it off avoids the accidental-
986 * highlight mess that happens when a drag starts on the clock. */
987 user-select: none;
988 -webkit-user-select: none;
989 }
990
991 .desktop-mode-widget-clock__time {
992 font-size: 34px;
993 font-weight: 600;
994 line-height: 1.1;
995 font-variant-numeric: tabular-nums;
996 letter-spacing: 0.01em;
997 }
998
999 .desktop-mode-widget-clock__date {
1000 margin-top: 4px;
1001 font-size: 13px;
1002 color: rgba( 255, 255, 255, 0.75 );
1003 letter-spacing: 0.02em;
1004 }
1005
1006 /* ---------------------------------------------------------------
1007 * Widget picker popover — opens from the add tile, lists every
1008 * registered widget. Positioned fixed so it can extend outside
1009 * the desktop-area clip.
1010 * --------------------------------------------------------------- */
1011
1012 .desktop-mode-widget-picker {
1013 position: fixed;
1014 width: 320px;
1015 max-height: 60vh;
1016 overflow-y: auto;
1017 padding: 10px;
1018 border-radius: 14px;
1019 background: rgba( 20, 20, 22, 0.88 );
1020 backdrop-filter: blur( 22px ) saturate( 160% );
1021 -webkit-backdrop-filter: blur( 22px ) saturate( 160% );
1022 border: 1px solid rgba( 255, 255, 255, 0.1 );
1023 box-shadow: 0 18px 54px rgba( 0, 0, 0, 0.55 );
1024 color: #fff;
1025 z-index: 9999;
1026 animation: desktop-mode-widget-picker-in 0.14s ease-out;
1027 }
1028
1029 @keyframes desktop-mode-widget-picker-in {
1030 from {
1031 opacity: 0;
1032 transform: translateY( 4px );
1033 }
1034 }
1035
1036 .desktop-mode-widget-picker__title {
1037 padding: 4px 8px 8px;
1038 font-size: 11px;
1039 font-weight: 600;
1040 letter-spacing: 0.08em;
1041 text-transform: uppercase;
1042 color: rgba( 255, 255, 255, 0.55 );
1043 }
1044
1045 .desktop-mode-widget-picker__list {
1046 display: flex;
1047 flex-direction: column;
1048 gap: 4px;
1049 }
1050
1051 .desktop-mode-widget-picker__entry {
1052 display: flex;
1053 align-items: center;
1054 gap: 12px;
1055 padding: 10px;
1056 background: transparent;
1057 border: 0;
1058 border-radius: 10px;
1059 color: #fff;
1060 cursor: pointer;
1061 text-align: start;
1062 font: inherit;
1063 transition: background-color 0.12s ease;
1064 }
1065
1066 .desktop-mode-widget-picker__entry:hover:not( :disabled ) {
1067 background: rgba( 255, 255, 255, 0.08 );
1068 }
1069
1070 .desktop-mode-widget-picker__entry:focus-visible {
1071 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
1072 outline-offset: 1px;
1073 }
1074
1075 .desktop-mode-widget-picker__entry--added {
1076 opacity: 0.55;
1077 cursor: default;
1078 }
1079
1080 .desktop-mode-widget-picker__entry-icon {
1081 flex-shrink: 0;
1082 font-size: 20px;
1083 width: 20px;
1084 height: 20px;
1085 line-height: 1;
1086 }
1087
1088 .desktop-mode-widget-picker__entry-text {
1089 display: flex;
1090 flex-direction: column;
1091 gap: 2px;
1092 flex: 1;
1093 min-width: 0;
1094 }
1095
1096 .desktop-mode-widget-picker__entry-label {
1097 font-size: 13px;
1098 font-weight: 500;
1099 white-space: nowrap;
1100 overflow: hidden;
1101 text-overflow: ellipsis;
1102 }
1103
1104 .desktop-mode-widget-picker__entry-description {
1105 font-size: 11px;
1106 color: rgba( 255, 255, 255, 0.6 );
1107 line-height: 1.35;
1108 white-space: normal;
1109 }
1110
1111 .desktop-mode-widget-picker__entry-status {
1112 flex-shrink: 0;
1113 font-size: 11px;
1114 color: rgba( 255, 255, 255, 0.5 );
1115 text-transform: uppercase;
1116 letter-spacing: 0.08em;
1117 }
1118
1119 .desktop-mode-widget-picker__empty {
1120 padding: 12px 8px;
1121 font-size: 12px;
1122 color: rgba( 255, 255, 255, 0.6 );
1123 line-height: 1.4;
1124 }
1125
1126 /**
1127 * Chromeless mode: pages loaded inside iframes.
1128 * Remove all shell chrome and let content fill the frame.
1129 */
1130 .desktop-mode-chromeless {
1131 margin: 0;
1132 padding: 0;
1133 background: var(--desktop-mode-window-bg);
1134 }
1135
1136 /*
1137 * Chromeless #wpbody-content, #adminmenuwrap, #wpfooter overrides are in
1138 * chromeless.css to keep all legacy page tweaks in one place.
1139 */
1140
1141 /*
1142 * Hide WordPress core's built-in command palette (`@wordpress/commands`).
1143 * Desktop mode force-enqueues `wp_enqueue_command_palette_assets()` so
1144 * the `core/commands` data store is populated for our shell harvester
1145 * (see `src/commands/shell-harvester.ts`); the side effect is that WP
1146 * also mounts its own `<CommandPalette>` to the document body. We
1147 * already block its Cmd+K handler in `src/palette-registry.ts`, but if
1148 * a third-party script opens it programmatically (`wp.data.dispatch(
1149 * 'core/commands' ).open()`), the dialog would float over the desktop
1150 * with admin-menu navigation callbacks whose `document.location = url`
1151 * would unload the shell. Keep it permanently hidden.
1152 */
1153 .commands-command-menu,
1154 .commands-command-menu__overlay {
1155 display: none !important;
1156 }
1157