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

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

1,188 lines 32.6 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 border-radius: 14px;
629 position: absolute;
630 top: 16px;
631 bottom: 16px;
632 inset-inline-end: 16px;
633 width: 320px;
634 display: flex;
635 flex-direction: column;
636 gap: 12px;
637 z-index: 1;
638 pointer-events: none;
639 overflow-y: auto;
640 /* Hide the scrollbar chrome unless the user actually overflows —
641 * most setups never will. */
642 scrollbar-width: thin;
643 /* Fade alongside the overview backdrop. The class flip that
644 * drives this is on the parent area, set at t=0 of enter and
645 * removed at t=0 of exit, so widgets fade in parallel with the
646 * dock collapse / return — never sequential. */
647 opacity: 1;
648 transition:
649 opacity 0.22s ease,
650 transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
651 }
652
653 .desktop-mode-area--overview .desktop-mode-widgets {
654 opacity: 0;
655 /* Slight slide-off-right so the fade reads as "the column is
656 * stepping back out of the way" rather than flat-dimming. */
657 transform: translateX( 16px );
658 pointer-events: none;
659 }
660
661 .desktop-mode-widgets__list {
662 display: flex;
663 flex-direction: column;
664 gap: 12px;
665 padding: 4px;
666 }
667
668 /*
669 * Individual widget card — glass backdrop matching the dock /
670 * overview top bar. Card root is pointer-events: auto so clicks on
671 * content + the remove X land; the column itself stays
672 * pointer-transparent so a drag that grazes the column's margin
673 * falls through to the window beneath.
674 */
675 .desktop-mode-widgets__card {
676 position: relative;
677 padding: 0;
678 /* Column layout so the body can flex-shrink and scroll when the
679 * frame writes a fixed inline height (floating / resized cards).
680 * Without this the body keeps its natural height and content
681 * spills past the card's rounded bottom edge. */
682 display: flex;
683 flex-direction: column;
684 pointer-events: auto;
685 background: rgba( 20, 20, 22, 0.55 );
686 backdrop-filter: blur( 18px ) saturate( 140% );
687 -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
688 border: 1px solid rgba( 255, 255, 255, 0.08 );
689 border-radius: 14px;
690 color: #fff;
691 box-shadow:
692 0 8px 28px rgba( 0, 0, 0, 0.35 ),
693 inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
694 transition:
695 box-shadow 0.18s ease,
696 opacity 0.18s ease;
697 }
698
699 /* Hover feedback is shadow-only — no transform. Cards must feel
700 * anchored; a hover nudge reads as the widget drifting under the
701 * pointer (and fought the user's chosen position on floating cards). */
702 .desktop-mode-widgets__card:hover {
703 box-shadow:
704 0 12px 36px rgba( 0, 0, 0, 0.45 ),
705 inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
706 }
707
708 .desktop-mode-widgets__card-body {
709 padding: 16px;
710 min-height: 48px;
711 flex: 1 1 auto;
712 overflow-x: hidden;
713 overflow-y: auto;
714 scrollbar-width: thin;
715 }
716
717 .desktop-mode-widgets__card-close {
718 position: absolute;
719 top: 6px;
720 inset-inline-end: 6px;
721 width: 22px;
722 height: 22px;
723 display: flex;
724 align-items: center;
725 justify-content: center;
726 padding: 0;
727 border: 0;
728 background: rgba( 0, 0, 0, 0.55 );
729 color: rgba( 255, 255, 255, 0.75 );
730 border-radius: 50%;
731 cursor: pointer;
732 opacity: 0;
733 transition:
734 opacity 0.15s ease,
735 background-color 0.15s ease,
736 color 0.15s ease;
737 }
738
739 .desktop-mode-widgets__card:hover .desktop-mode-widgets__card-close,
740 .desktop-mode-widgets__card-close:focus-visible {
741 opacity: 1;
742 }
743
744 .desktop-mode-widgets__card-close:hover {
745 background: #d63638;
746 color: #fff;
747 }
748
749 /*
750 * Movable widgets — a thin chrome header at the top of the card holds
751 * the drag grip, the widget's own label, and the × button. The whole
752 * header is the drag handle (except for interactive children — those
753 * remain clickable as normal). Non-movable widgets keep today's
754 * corner-× layout unchanged.
755 */
756 .desktop-mode-widgets__chrome {
757 display: flex;
758 flex-shrink: 0;
759 align-items: center;
760 gap: 8px;
761 padding: 6px 8px;
762 border-bottom: 1px solid rgba( 255, 255, 255, 0.06 );
763 cursor: grab;
764 user-select: none;
765 -webkit-user-select: none;
766 touch-action: none;
767 }
768
769 .desktop-mode-widgets__card--dragging .desktop-mode-widgets__chrome {
770 cursor: grabbing;
771 }
772
773 .desktop-mode-widgets__grip {
774 flex-shrink: 0;
775 width: 10px;
776 height: 16px;
777 background-image: radial-gradient(
778 circle,
779 rgba( 255, 255, 255, 0.55 ) 1.2px,
780 transparent 1.5px
781 );
782 background-size: 5px 5px;
783 background-position: 0 1px;
784 background-repeat: space;
785 opacity: 0.45;
786 transition: opacity 0.12s ease;
787 }
788
789 .desktop-mode-widgets__card:hover .desktop-mode-widgets__grip {
790 opacity: 0.85;
791 }
792
793 .desktop-mode-widgets__title {
794 flex: 1;
795 font-size: 12px;
796 font-weight: 500;
797 letter-spacing: 0.01em;
798 color: rgba( 255, 255, 255, 0.85 );
799 overflow: hidden;
800 text-overflow: ellipsis;
801 white-space: nowrap;
802 }
803
804 /* When the close button sits inside the chrome it's inline, not
805 corner-absolute — reset position + always-visible opacity. */
806 .desktop-mode-widgets__chrome .desktop-mode-widgets__card-close {
807 position: static;
808 opacity: 0.7;
809 width: 20px;
810 height: 20px;
811 }
812
813 .desktop-mode-widgets__chrome .desktop-mode-widgets__card-close:hover,
814 .desktop-mode-widgets__chrome .desktop-mode-widgets__card-close:focus-visible {
815 opacity: 1;
816 }
817
818 /*
819 * Re-dock button — sits in the chrome next to the close button and
820 * returns a floating widget to the right column. Visibility is
821 * gated on the `--floating` modifier class so docked widgets never
822 * expose it (tapping it on a docked card would be a confusing
823 * no-op). The DOM node stays mounted either way so state flips
824 * don't rebuild chrome.
825 *
826 * Styling mirrors the inline close button: 20×20 round tile,
827 * dimmed by default, full-opacity on hover / focus. Distinct hover
828 * color (theme accent, not red) signals "put back" vs. "remove."
829 */
830 .desktop-mode-widgets__card-redock {
831 display: none;
832 align-items: center;
833 justify-content: center;
834 padding: 0;
835 border: 0;
836 background: rgba( 0, 0, 0, 0.55 );
837 color: rgba( 255, 255, 255, 0.75 );
838 border-radius: 50%;
839 width: 20px;
840 height: 20px;
841 cursor: pointer;
842 opacity: 0.7;
843 transition:
844 opacity 0.15s ease,
845 background-color 0.15s ease,
846 color 0.15s ease;
847 }
848
849 .desktop-mode-widgets__card--floating .desktop-mode-widgets__card-redock {
850 display: flex;
851 }
852
853 .desktop-mode-widgets__card-redock:hover,
854 .desktop-mode-widgets__card-redock:focus-visible {
855 opacity: 1;
856 background: var( --wp-admin-theme-color, #2271b1 );
857 color: #fff;
858 }
859
860 /*
861 * Floating widgets — absolute-positioned, inline top/left/width/height
862 * written by the frame.
863 */
864 .desktop-mode-widgets__card--floating {
865 position: absolute;
866 margin: 0;
867 }
868
869 .desktop-mode-widgets__card--dragging,
870 .desktop-mode-widgets__card--resizing {
871 transition: none;
872 z-index: 2;
873 }
874
875 /*
876 * Resize handles — 8 in all, with the 4 edges as hair-line strips and
877 * the 4 corners as small squares. Invisible by default; cursor change
878 * telegraphs them on hover. The frame attaches pointer listeners to
879 * every handle; a non-movable widget ignores everything except the
880 * `s` (bottom-edge, height-only) handle at the pointer layer, so we
881 * visually hide the mismatched handles via the modifier class.
882 */
883 .desktop-mode-widgets__resize {
884 position: absolute;
885 z-index: 1;
886 }
887
888 .desktop-mode-widgets__resize--n {
889 top: -3px;
890 left: 8px;
891 right: 8px;
892 height: 6px;
893 cursor: ns-resize;
894 }
895
896 .desktop-mode-widgets__resize--s {
897 bottom: -3px;
898 left: 8px;
899 right: 8px;
900 height: 6px;
901 cursor: ns-resize;
902 }
903
904 .desktop-mode-widgets__resize--e {
905 top: 8px;
906 bottom: 8px;
907 right: -3px;
908 width: 6px;
909 cursor: ew-resize;
910 }
911
912 .desktop-mode-widgets__resize--w {
913 top: 8px;
914 bottom: 8px;
915 left: -3px;
916 width: 6px;
917 cursor: ew-resize;
918 }
919
920 .desktop-mode-widgets__resize--ne {
921 top: -4px;
922 right: -4px;
923 width: 12px;
924 height: 12px;
925 cursor: nesw-resize;
926 }
927
928 .desktop-mode-widgets__resize--nw {
929 top: -4px;
930 left: -4px;
931 width: 12px;
932 height: 12px;
933 cursor: nwse-resize;
934 }
935
936 .desktop-mode-widgets__resize--se {
937 bottom: -4px;
938 right: -4px;
939 width: 12px;
940 height: 12px;
941 cursor: nwse-resize;
942 }
943
944 .desktop-mode-widgets__resize--sw {
945 bottom: -4px;
946 left: -4px;
947 width: 12px;
948 height: 12px;
949 cursor: nesw-resize;
950 }
951
952 /*
953 * Only floating cards expose the full 8-direction handle set. A
954 * column-docked card — non-movable OR movable-but-not-yet-liberated —
955 * resizes on the height axis alone (the frame ignores every other
956 * direction while docked), so hide all handles except the bottom
957 * edge and the cursor never lies about what's interactive.
958 */
959 .desktop-mode-widgets__card--resizable:not( .desktop-mode-widgets__card--floating ) .desktop-mode-widgets__resize:not( .desktop-mode-widgets__resize--s ) {
960 display: none;
961 }
962
963 /*
964 * Add-widget tile — matches the "+" tile in the overview top bar.
965 * Always visible at the bottom of the column; promoted to center
966 * when the column is otherwise empty so first-run users see a
967 * friendly target rather than an empty strip.
968 */
969 .desktop-mode-widgets__add {
970 margin-top: auto;
971 padding: 14px 12px;
972 display: flex;
973 align-items: center;
974 justify-content: center;
975 gap: 8px;
976 pointer-events: auto;
977 background: transparent;
978 border: 2px dashed rgba( 255, 255, 255, 0.22 );
979 border-radius: 14px;
980 color: rgba( 255, 255, 255, 0.72 );
981 font: inherit;
982 font-weight: 500;
983 cursor: pointer;
984 transition:
985 border-color 0.15s ease,
986 background-color 0.15s ease,
987 color 0.15s ease;
988 }
989
990 .desktop-mode-widgets__add:hover {
991 border-color: rgba( 255, 255, 255, 0.4 );
992 background: rgba( 255, 255, 255, 0.06 );
993 color: #fff;
994 }
995
996 .desktop-mode-widgets__add:focus-visible {
997 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
998 outline-offset: 2px;
999 }
1000
1001 .desktop-mode-widgets__add-plus {
1002 font-size: 20px;
1003 line-height: 1;
1004 font-weight: 200;
1005 }
1006
1007 .desktop-mode-widgets__add-label {
1008 font-size: 13px;
1009 letter-spacing: 0.02em;
1010 }
1011
1012 /* Built-in clock widget */
1013 .desktop-mode-widget-clock {
1014 text-align: center;
1015 /* The time + date are passive glyphs, not content users would
1016 * ever reasonably select — turning it off avoids the accidental-
1017 * highlight mess that happens when a drag starts on the clock. */
1018 user-select: none;
1019 -webkit-user-select: none;
1020 }
1021
1022 .desktop-mode-widget-clock__time {
1023 font-size: 34px;
1024 font-weight: 600;
1025 line-height: 1.1;
1026 font-variant-numeric: tabular-nums;
1027 letter-spacing: 0.01em;
1028 }
1029
1030 .desktop-mode-widget-clock__date {
1031 margin-top: 4px;
1032 font-size: 13px;
1033 color: rgba( 255, 255, 255, 0.75 );
1034 letter-spacing: 0.02em;
1035 }
1036
1037 /* ---------------------------------------------------------------
1038 * Widget picker popover — opens from the add tile, lists every
1039 * registered widget. Positioned fixed so it can extend outside
1040 * the desktop-area clip.
1041 * --------------------------------------------------------------- */
1042
1043 .desktop-mode-widget-picker {
1044 position: fixed;
1045 width: 320px;
1046 max-height: 60vh;
1047 overflow-y: auto;
1048 padding: 10px;
1049 border-radius: 14px;
1050 background: rgba( 20, 20, 22, 0.88 );
1051 backdrop-filter: blur( 22px ) saturate( 160% );
1052 -webkit-backdrop-filter: blur( 22px ) saturate( 160% );
1053 border: 1px solid rgba( 255, 255, 255, 0.1 );
1054 box-shadow: 0 18px 54px rgba( 0, 0, 0, 0.55 );
1055 color: #fff;
1056 z-index: 9999;
1057 animation: desktop-mode-widget-picker-in 0.14s ease-out;
1058 }
1059
1060 @keyframes desktop-mode-widget-picker-in {
1061 from {
1062 opacity: 0;
1063 transform: translateY( 4px );
1064 }
1065 }
1066
1067 .desktop-mode-widget-picker__title {
1068 padding: 4px 8px 8px;
1069 font-size: 11px;
1070 font-weight: 600;
1071 letter-spacing: 0.08em;
1072 text-transform: uppercase;
1073 color: rgba( 255, 255, 255, 0.55 );
1074 }
1075
1076 .desktop-mode-widget-picker__list {
1077 display: flex;
1078 flex-direction: column;
1079 gap: 4px;
1080 }
1081
1082 .desktop-mode-widget-picker__entry {
1083 display: flex;
1084 align-items: center;
1085 gap: 12px;
1086 padding: 10px;
1087 background: transparent;
1088 border: 0;
1089 border-radius: 10px;
1090 color: #fff;
1091 cursor: pointer;
1092 text-align: start;
1093 font: inherit;
1094 transition: background-color 0.12s ease;
1095 }
1096
1097 .desktop-mode-widget-picker__entry:hover:not( :disabled ) {
1098 background: rgba( 255, 255, 255, 0.08 );
1099 }
1100
1101 .desktop-mode-widget-picker__entry:focus-visible {
1102 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
1103 outline-offset: 1px;
1104 }
1105
1106 .desktop-mode-widget-picker__entry--added {
1107 opacity: 0.55;
1108 cursor: default;
1109 }
1110
1111 .desktop-mode-widget-picker__entry-icon {
1112 flex-shrink: 0;
1113 font-size: 20px;
1114 width: 20px;
1115 height: 20px;
1116 line-height: 1;
1117 }
1118
1119 .desktop-mode-widget-picker__entry-text {
1120 display: flex;
1121 flex-direction: column;
1122 gap: 2px;
1123 flex: 1;
1124 min-width: 0;
1125 }
1126
1127 .desktop-mode-widget-picker__entry-label {
1128 font-size: 13px;
1129 font-weight: 500;
1130 white-space: nowrap;
1131 overflow: hidden;
1132 text-overflow: ellipsis;
1133 }
1134
1135 .desktop-mode-widget-picker__entry-description {
1136 font-size: 11px;
1137 color: rgba( 255, 255, 255, 0.6 );
1138 line-height: 1.35;
1139 white-space: normal;
1140 }
1141
1142 .desktop-mode-widget-picker__entry-status {
1143 flex-shrink: 0;
1144 font-size: 11px;
1145 color: rgba( 255, 255, 255, 0.5 );
1146 text-transform: uppercase;
1147 letter-spacing: 0.08em;
1148 }
1149
1150 .desktop-mode-widget-picker__empty {
1151 padding: 12px 8px;
1152 font-size: 12px;
1153 color: rgba( 255, 255, 255, 0.6 );
1154 line-height: 1.4;
1155 }
1156
1157 /**
1158 * Chromeless mode: pages loaded inside iframes.
1159 * Remove all shell chrome and let content fill the frame.
1160 */
1161 .desktop-mode-chromeless {
1162 margin: 0;
1163 padding: 0;
1164 background: var(--desktop-mode-window-bg);
1165 }
1166
1167 /*
1168 * Chromeless #wpbody-content, #adminmenuwrap, #wpfooter overrides are in
1169 * chromeless.css to keep all legacy page tweaks in one place.
1170 */
1171
1172 /*
1173 * Hide WordPress core's built-in command palette (`@wordpress/commands`).
1174 * Desktop mode force-enqueues `wp_enqueue_command_palette_assets()` so
1175 * the `core/commands` data store is populated for our shell harvester
1176 * (see `src/commands/shell-harvester.ts`); the side effect is that WP
1177 * also mounts its own `<CommandPalette>` to the document body. We
1178 * already block its Cmd+K handler in `src/palette-registry.ts`, but if
1179 * a third-party script opens it programmatically (`wp.data.dispatch(
1180 * 'core/commands' ).open()`), the dialog would float over the desktop
1181 * with admin-menu navigation callbacks whose `document.location = url`
1182 * would unload the shell. Keep it permanently hidden.
1183 */
1184 .commands-command-menu,
1185 .commands-command-menu__overlay {
1186 display: none !important;
1187 }
1188