PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.1
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.9.1, at assets/css/desktop.css

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