PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.6
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / assets / css / desktop-files.css

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

1,274 lines 36.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OpenStation — Files-on-the-Desktop tile + layer styles.
3 *
4 * The layer is an absolutely-positioned container inside
5 * `#os-area`; tiles inside use absolute positioning
6 * keyed off `(x, y)` coordinates persisted server-side.
7 *
8 * Tiles are intentionally close to the look of `os-icon`
9 * (the plugin-shortcut tiles) so the desktop reads as one
10 * coherent surface — but they live in their own class namespace
11 * (`os-file-tile`) so plugins can theme each rail
12 * independently.
13 *
14 * @since 0.9.0
15 */
16
17 /*
18 * Unified rail (since 0.9.0): plugin shortcuts now ride on the
19 * same files layer as folders / posts / etc — they're a
20 * `'shortcut'` file type. The legacy `.os-icons` DIV
21 * still renders for backwards compatibility (existing tests and
22 * non-files-active code paths), but when a files layer is
23 * mounted as a sibling we hide it so we don't end up with two
24 * rails competing for the same wallpaper.
25 */
26 #os-area:has( > .os-files-layer ) > .os-icons {
27 display: none;
28 }
29
30 /*
31 * Drop-target highlight — the `data-files-drop-active` attribute
32 * is set on the FilesLayer host (desktop area or folder window
33 * body) while a shortcut payload is being dragged over it.
34 *
35 * Marching-ants animation makes the affordance unmissable even
36 * against the busiest wallpaper, and the soft inner glow draws
37 * the eye when the user is "shopping" for a drop target. Both
38 * effects respect prefers-reduced-motion below.
39 *
40 * @since 0.18.0; visuals strengthened in 0.20.0.
41 */
42 @keyframes os-drop-marching-ants {
43 to {
44 background-position: 24px 0, -24px 0, 0 24px, 0 -24px;
45 }
46 }
47
48 [ data-files-drop-active ] {
49 position: relative;
50 outline: 3px dashed var( --wp-admin-theme-color, #2271b1 );
51 outline-offset: -8px;
52 background-image:
53 linear-gradient(
54 90deg,
55 rgba( 34, 113, 177, 0.45 ) 50%,
56 transparent 0
57 ),
58 linear-gradient(
59 90deg,
60 rgba( 34, 113, 177, 0.45 ) 50%,
61 transparent 0
62 ),
63 linear-gradient(
64 0deg,
65 rgba( 34, 113, 177, 0.45 ) 50%,
66 transparent 0
67 ),
68 linear-gradient(
69 0deg,
70 rgba( 34, 113, 177, 0.45 ) 50%,
71 transparent 0
72 );
73 background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
74 background-size: 12px 3px, 12px 3px, 3px 12px, 3px 12px;
75 background-position: 0 6px, 0 calc( 100% - 6px ), 6px 0, calc( 100% - 6px ) 0;
76 box-shadow: inset 0 0 32px 4px rgba( 34, 113, 177, 0.12 );
77 animation: os-drop-marching-ants 0.6s linear infinite;
78 transition: box-shadow 0.18s ease-out;
79 }
80
81 @media ( prefers-reduced-motion: reduce ) {
82 [ data-files-drop-active ] {
83 animation: none;
84 }
85 }
86
87 .os-files-layer {
88 position: absolute;
89 inset: 0;
90 z-index: 0;
91 /*
92 * Layer doesn't intercept pointer events itself — tiles and
93 * the wallpaper own that. Phase 4's wallpaper context menu
94 * binds clicks on the parent `#os-area`, which is
95 * unblocked because of this rule.
96 */
97 pointer-events: none;
98 }
99
100 /*
101 * Live drop-cell preview. A soft outline that hovers at the cell
102 * the dropped tile will land in, so the user can predict where the
103 * grid snap will place it before releasing. Positioned absolutely
104 * inside the files layer; `translate3d` updated from JS on every
105 * pointermove while a desktop-file drag hovers the canvas.
106 *
107 * The tile dimensions are baked into `grid.ts` (88 × 96 visual,
108 * 96 × 110 cell pitch). The outline matches the cell pitch — slightly
109 * larger than the tile — so the highlight reads as "this slot" rather
110 * than "this tile."
111 *
112 * @since 0.20.0
113 */
114 .os-files-drop-preview {
115 position: absolute;
116 top: 0;
117 left: 0;
118 width: 88px;
119 height: 96px;
120 pointer-events: none;
121 box-sizing: border-box;
122 border-radius: 12px;
123 background: var(
124 --os-drop-preview-bg,
125 rgba( 34, 113, 177, 0.08 )
126 );
127 border: 2px dashed
128 var(
129 --os-drop-preview-border,
130 rgba( 34, 113, 177, 0.55 )
131 );
132 transition: transform 90ms ease-out;
133 will-change: transform;
134 /* Sit BELOW tiles so a tile being dragged over the cell isn't
135 * visually clipped by the dashed outline; the outline still reads
136 * because the tile is semi-transparent during drag (`--dragging`
137 * sets `opacity: 0.4`). */
138 z-index: 0;
139 }
140
141 @media ( prefers-reduced-motion: reduce ) {
142 .os-files-drop-preview {
143 transition: none;
144 }
145 }
146
147 .os-file-tile {
148 pointer-events: auto;
149 position: absolute;
150 display: flex;
151 flex-direction: column;
152 align-items: center;
153 justify-content: flex-start;
154 gap: 6px;
155 /*
156 * `box-sizing` is load-bearing, not housekeeping. Without it the
157 * horizontal padding is ADDED to the declared width, so an 88px
158 * tile occupies 96px — which was exactly the desktop's old cell
159 * pitch, and why icons sat edge to edge with no gap at all while
160 * the maths insisted there were 8px between them.
161 *
162 * The tile is the size the grid says it is. Padding lives inside.
163 */
164 box-sizing: border-box;
165 width: var( --os-tile-w, 88px );
166 /*
167 * Fixed, not minimum. The tile box IS the selection ring, and a
168 * box that grows with its label gives a row of selected icons a
169 * ragged top edge — one height per label that happened to wrap
170 * to two lines. Every tile occupies its cell; short labels leave
171 * the slack inside the ring, where it reads as padding.
172 */
173 height: var( --os-tile-h, 104px );
174 padding: 8px 4px;
175 border: 0;
176 background: transparent;
177 /* Tile foreground driven by `--os-tile-fg` so any
178 * light-on-dark surface (folder window, My WordPress preview)
179 * can retint tiles by overriding the variable in its scope —
180 * no per-surface selectors needed here. */
181 color: var( --os-tile-fg, var( --os-fg, #fff ) );
182 cursor: pointer;
183 border-radius: 8px;
184 transition: background-color 0.15s ease, transform 0.05s linear;
185 -webkit-user-select: none;
186 user-select: none;
187 touch-action: none;
188 /* Soft fade-in on mount — each tile carries its own randomised
189 * `--enter-delay` / `--enter-duration` (set inline by the
190 * tile builder), so a freshly-painted grid reads as a cascade
191 * rather than a synchronised pop-in. Both values land in the
192 * decimal-of-a-second range — perceivable, never sluggish.
193 *
194 * `backwards` keeps the tile invisible during its delay so the
195 * cascade is honest; the animation runs once, leaves the tile
196 * at its static `opacity: 1`, and never re-fires from later
197 * state changes (drag, hover, drop-target).
198 */
199 animation: os-file-tile-enter
200 var( --os-file-tile-enter-duration, 0.4s )
201 ease-out
202 var( --os-file-tile-enter-delay, 0s )
203 backwards;
204 }
205
206 @keyframes os-file-tile-enter {
207 from {
208 opacity: 0;
209 }
210 to {
211 opacity: 1;
212 }
213 }
214
215 @media ( prefers-reduced-motion: reduce ) {
216 .os-file-tile {
217 animation: none;
218 }
219 }
220
221 .os-file-tile:hover,
222 .os-file-tile:focus-visible {
223 background: var(
224 --os-tile-hover-bg,
225 rgba( 255, 255, 255, 0.12 )
226 );
227 outline: none;
228 }
229
230 .os-file-tile:focus-visible {
231 /* Inset focus ring — outset rings extended past the tile and
232 * visually overlapped the colindant icon when the canvas cell
233 * pitch is tight (96 px cell, 88 px tile → only 4 px clearance
234 * per side). Inset keeps the ring fully inside the tile. */
235 box-shadow: inset 0 0 0 2px
236 var( --os-tile-focus-ring, var( --wp-admin-theme-color, #2271b1 ) );
237 }
238
239 /* Status ribbon sizing inside tiles. The `<os-ribbon>` defaults
240 * are tuned for cards (~90 px clipping window); 88 px-wide tiles
241 * need a tighter footprint or the ribbon dominates the icon. We
242 * override the component's CSS custom-property theming surface so
243 * the ribbon stays a small accent in the corner. Authors who want
244 * different sizing can override per-tile or per-section. */
245 .os-file-tile > os-ribbon {
246 --os-ui-ribbon-size: 48px;
247 --os-ui-ribbon-banner-width: 72px;
248 --os-ui-ribbon-banner-offset: 12px;
249 --os-ui-ribbon-banner-pull: -18px;
250 --os-ui-ribbon-padding: 1px 0;
251 --os-ui-ribbon-font: 700 7px/1.4 var( --os-font, system-ui );
252 --os-ui-ribbon-tracking: 0.05em;
253 --os-ui-ribbon-shadow: 0 1px 2px rgba( 0, 0, 0, 0.2 );
254 }
255
256 .os-file-tile--dragging {
257 /* The DragManager renders a separate ghost element that follows
258 * the pointer; the SOURCE tile stays in place but is dimmed so
259 * the user has a stable reference for "this is what I'm
260 * dragging."
261 *
262 * @since 0.18.0
263 */
264 opacity: 0.4;
265 cursor: grabbing;
266 }
267
268 /* Pinned tiles ("My WordPress", Recycle Bin) anchor to a fixed slot
269 * and DO NOT wire drag — but they look + behave identically to any
270 * other tile until the user attempts a drag (which silently fails).
271 * No upfront visual cue: feedback now happens at the moment of the
272 * gesture, not before, per @since 0.9.0 design feedback.
273 *
274 * Default `cursor: pointer` from the base `.os-file-tile`
275 * rule covers click-to-open. Touch interactions stay as
276 * `touch-action: auto` so the OS scroll gesture works on top of
277 * the tile.
278 */
279 .os-file-tile--pinned {
280 touch-action: auto;
281 }
282
283 /* Folder tile is the drop target during a cross-window shortcut
284 * drag — the user dragged a post / page / user tile out of My
285 * WordPress and is hovering over a folder. Bright outline, soft
286 * pulse, and a slight scale so the folder reads unmistakably as
287 * "yes, drop here." @since 0.8.0; pulse added 0.20.0. */
288 @keyframes os-drop-target-pulse {
289 0% {
290 box-shadow:
291 0 0 0 0 rgba( 34, 113, 177, 0.5 ),
292 inset 0 0 0 2px var( --wp-admin-theme-color, #2271b1 );
293 }
294 100% {
295 box-shadow:
296 0 0 0 8px rgba( 34, 113, 177, 0 ),
297 inset 0 0 0 2px var( --wp-admin-theme-color, #2271b1 );
298 }
299 }
300
301 .os-file-tile--drop-target {
302 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
303 outline-offset: -4px;
304 background: rgba( 34, 113, 177, 0.2 );
305 transform: scale( 1.08 );
306 z-index: 5;
307 animation: os-drop-target-pulse 1s ease-out infinite;
308 border-radius: 12px;
309 }
310
311 @media ( prefers-reduced-motion: reduce ) {
312 .os-file-tile--drop-target {
313 animation: none;
314 }
315 }
316
317 /* Drop-hint chip — floats next to the cursor during a drag and
318 * shows context-aware text ("Drop here to create shortcut",
319 * "Can't drop here", etc.). The chip text is owned by the
320 * ghost module (`src/drag/ghost.ts`); these styles control the
321 * appearance per accept/reject/neutral mode.
322 *
323 * @since 0.20.0
324 */
325 .os-drag-hint {
326 position: fixed;
327 top: 0;
328 left: 0;
329 padding: 6px 10px;
330 border-radius: 999px;
331 font-size: 12px;
332 font-weight: 600;
333 letter-spacing: 0.02em;
334 white-space: nowrap;
335 pointer-events: none;
336 user-select: none;
337 box-shadow: 0 4px 14px rgba( 0, 0, 0, 0.22 );
338 background: rgba( 30, 30, 30, 0.85 );
339 color: var( --os-ui-fg-on-accent, #fff );
340 z-index: 2147483647;
341 transition:
342 background-color 0.12s ease,
343 color 0.12s ease,
344 transform 0.06s ease-out;
345 }
346
347 .os-drag-hint--neutral {
348 background: rgba( 30, 30, 30, 0.85 );
349 color: var( --os-ui-fg-on-accent, #fff );
350 }
351
352 .os-drag-hint--accept {
353 background: #1e8449;
354 color: var( --os-ui-fg-on-accent, #fff );
355 box-shadow:
356 0 0 0 2px rgba( 30, 132, 73, 0.25 ),
357 0 6px 18px rgba( 30, 132, 73, 0.3 );
358 }
359
360 .os-drag-hint--accept::before {
361 /* Tiny checkmark chevron prefix so "Drop here" reads as an
362 * actionable affirmation rather than a passive label. The
363 * checkmark is a real character (U+2713) so screen readers
364 * skip past it via the `aria-hidden` on the host chip. */
365 content: '✓ ';
366 }
367
368 .os-drag-hint--reject {
369 background: var( --os-ui-danger-hover, #b32d2e );
370 color: var( --os-ui-fg-on-accent, #fff );
371 box-shadow:
372 0 0 0 2px rgba( 179, 45, 46, 0.25 ),
373 0 6px 18px rgba( 179, 45, 46, 0.3 );
374 }
375
376 .os-drag-hint--reject::before {
377 content: '⊘ ';
378 }
379
380 /*
381 * No body-level cursor swap during drag — the user prefers the
382 * default arrow cursor regardless of state, so the accept/reject
383 * feedback lives entirely in the hint chip + outline animation.
384 * Don't reintroduce a `cursor: grabbing/copy/no-drop` here.
385 */
386
387 /* Shortcut overlay — small arrow badge in the BOTTOM-RIGHT corner
388 * of the icon visual on any tile that's a reference to an external
389 * entity (post, attachment, comment, user, term — anything that
390 * ISN'T a folder or a plugin shortcut). Mirrors the macOS / Windows
391 * convention of an arrow decoration on alias/shortcut icons.
392 *
393 * The geometry: tile is 88px wide with `padding: 8px 4px`. The icon
394 * visual is 48×48 centered horizontally, so the icon's bottom-right
395 * corner lands at roughly (68, 56) from the tile's top-left. The
396 * 18×18 arrow circle is anchored just inside that corner so it
397 * reads as part of the icon, not floating off in space.
398 *
399 * @since 0.8.0
400 */
401 /* Only real placements get the badge — i.e. tiles built by
402 * `buildTile(placement)` on the desktop / folder windows, which
403 * are the only surface where "this is a shortcut" actually means
404 * something. Tiles in My WordPress grids (and any future content
405 * surface) are the entity, not a shortcut to it, so the arrow
406 * would just be noise. `[data-placement-id]` is the canonical
407 * placement marker. */
408 .os-file-tile[ data-placement-id ][ data-file-type="post" ]::after,
409 .os-file-tile[ data-placement-id ][ data-file-type="attachment" ]::after,
410 .os-file-tile[ data-placement-id ][ data-file-type="comment" ]::after,
411 .os-file-tile[ data-placement-id ][ data-file-type="user" ]::after,
412 .os-file-tile[ data-placement-id ][ data-file-type="term" ]::after {
413 content: "↗";
414 position: absolute;
415 right: 14px;
416 top: 44px;
417 width: 18px;
418 height: 18px;
419 border-radius: 50%;
420 background: var( --os-tile-shortcut-bg, rgba( 0, 0, 0, 0.65 ) );
421 color: var( --os-tile-shortcut-fg, #fff );
422 font-size: 11px;
423 line-height: 18px;
424 text-align: center;
425 box-shadow: var(
426 --os-tile-shortcut-shadow,
427 0 1px 3px rgba( 0, 0, 0, 0.5 )
428 );
429 pointer-events: none;
430 }
431
432 /*
433 * Missing files (the underlying entity was deleted but the
434 * placement still exists) render the tile dimmed so the user
435 * can right-click → Remove without confusing it for an active
436 * file. Phase 6's per-placement permission re-check uses the
437 * same affordance for "you can't see this" placeholders.
438 */
439 .os-file-tile--missing {
440 opacity: 0.5;
441 }
442
443 /**
444 * Access-gated tile — the recipient sees the icon (the owner
445 * shared the folder containing it) but lacks `can_read` on the
446 * underlying entity. Dim the tile, kill the pointer affordance,
447 * and overlay a lock badge so the state reads at a glance.
448 */
449 .os-file-tile--access-gated {
450 opacity: 0.65;
451 cursor: not-allowed;
452 }
453 .os-file-tile--access-gated .os-file-tile__visual,
454 .os-file-tile--access-gated .os-file-tile__preview {
455 filter: grayscale( 0.6 );
456 }
457 .os-file-tile--access-gated:hover,
458 .os-file-tile--access-gated:focus-visible {
459 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) );
460 }
461 .os-file-tile__lock {
462 position: absolute;
463 top: 4px;
464 inset-inline-end: 4px;
465 width: 20px;
466 height: 20px;
467 border-radius: 50%;
468 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.7 ) );
469 color: var( --os-ui-fg-on-accent, #fff );
470 font-size: 14px;
471 line-height: 20px;
472 text-align: center;
473 pointer-events: none;
474 box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.5 );
475 }
476
477 /**
478 * Preview-pane empty state when a recipient selects a tile they
479 * can't open. Big lock ring + clear copy + secondary hint. Designed
480 * to read at a glance in the otherwise-busy preview column.
481 */
482 .os-files__access-gated {
483 display: flex;
484 flex-direction: column;
485 align-items: center;
486 justify-content: center;
487 gap: 16px;
488 padding: 32px 24px;
489 min-height: 240px;
490 text-align: center;
491 color: var( --os-fg, inherit );
492 }
493 .os-files__access-gated-ring {
494 display: flex;
495 align-items: center;
496 justify-content: center;
497 width: 96px;
498 height: 96px;
499 border-radius: 50%;
500 background: linear-gradient(
501 145deg,
502 rgba( 214, 54, 56, 0.22 ),
503 rgba( 214, 54, 56, 0.08 )
504 );
505 border: 2px solid rgba( 214, 54, 56, 0.45 );
506 box-shadow:
507 inset 0 1px 0 rgba( 255, 255, 255, 0.08 ),
508 0 8px 24px rgba( 214, 54, 56, 0.18 );
509 }
510 .os-files__access-gated-glyph {
511 font-size: 44px;
512 width: 44px;
513 height: 44px;
514 color: #ff8080;
515 filter: drop-shadow( 0 1px 2px rgba( 0, 0, 0, 0.4 ) );
516 }
517 .os-files__access-gated-title {
518 margin: 0;
519 font-size: 17px;
520 font-weight: 600;
521 letter-spacing: 0.2px;
522 }
523 .os-files__access-gated-sub {
524 margin: 0;
525 font-size: 13px;
526 line-height: 1.5;
527 max-width: 340px;
528 opacity: 0.85;
529 }
530 .os-files__access-gated-hint {
531 margin: 0;
532 font-size: 12px;
533 line-height: 1.5;
534 max-width: 340px;
535 opacity: 0.6;
536 }
537
538 .os-file-tile__visual {
539 display: inline-flex;
540 align-items: center;
541 justify-content: center;
542 width: 48px;
543 height: 48px;
544 font-size: 32px;
545 line-height: 1;
546 }
547
548 .os-file-tile__icon {
549 display: inline-flex;
550 align-items: center;
551 justify-content: center;
552 width: 48px;
553 height: 48px;
554 font-size: 32px;
555 line-height: 1;
556 }
557
558 .os-file-tile__preview {
559 width: 48px;
560 height: 48px;
561 object-fit: cover;
562 border-radius: 4px;
563 }
564
565 /* `<img>` defaults to `draggable=true`, which lets the browser
566 * start a native HTML5 image-drag from a pointerdown — that
567 * pre-empts the DragManager's pointer-event-driven tile rearrange
568 * and the tile silently refuses to move. The renderer also sets
569 * `draggable="false"` on every `<img>` it produces; this CSS rule
570 * covers anything injected later (plugin filters, decorators)
571 * without having to remember the attribute. */
572 .os-file-tile img {
573 -webkit-user-drag: none;
574 user-select: none;
575 }
576
577 /*
578 * Wallpaper + tile context menus use `<os-context-menu>` and
579 * `<os-context-menu-option>` (Phase 4 / 0.9.0). Their host-level
580 * styles live inside the component shadow DOM — see
581 * `src/ui/components/os-context-menu/os-context-menu.styles.ts`.
582 * No project CSS rules needed here; the legacy `.os-
583 * wallpaper-menu*` rules were removed when the components landed.
584 */
585
586 /*
587 * Folder native window — the body of `os-folder-<id>`
588 * contains a `FilesLayer` rendering that folder's contents.
589 * We ensure the body has positioning context so absolutely-
590 * positioned tiles inside the layer anchor correctly.
591 */
592 .desktop-mode-folder-window {
593 position: relative;
594 min-height: 200px;
595 /* Same window background recipe as My WordPress — defaults to
596 * the standard window bg (#fff) and themable per-window. */
597 background: var(
598 --os-folder-window-bg,
599 var( --os-window-bg, #fff )
600 );
601 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
602 display: flex;
603 flex-direction: column;
604 /* Light-on-dark context: the folder window opens inside a
605 * standard window chrome whose body is white-ish. Retint the
606 * tile color tokens here so dashicons + labels read against
607 * a light background. Plugins / themes can override these
608 * same variables at any scope to retint further. */
609 --os-tile-fg: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
610 /* Chains through the palette for the same reason as
611 * `--os-tile-fg` beside it: a bare literal here outranks the
612 * palette's own value and pins every tile's secondary line to
613 * near-black, which disappears the moment the surface is dark.
614 * The literal stays as the pre-brand floor. */
615 --os-tile-fg-muted: var(
616 --os-folder-window-fg-muted,
617 var( --os-ui-fg-muted, rgba( 0, 0, 0, 0.55 ) )
618 );
619 --os-tile-hover-bg: rgba( 0, 0, 0, 0.06 );
620 /* Light-context label rendering — share the same recipe with
621 * every other light-bg tile surface (My WordPress, future
622 * windows). One set of tokens, no per-surface duplication. */
623 --os-tile-label-shadow: none;
624 --os-tile-label-weight: 500;
625 --os-tile-label-smoothing: antialiased;
626 --os-tile-label-color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
627 }
628
629 /* Folder window's bottom status bar — paint with the same light-
630 * context treatment My WordPress uses (transparent bg, soft border,
631 * muted dark text). Without this, the wallpaper-targeted defaults
632 * (`background: rgba(0,0,0,0.18); color: white`) would show as a
633 * dark band against the white window bg. */
634 .desktop-mode-folder-window .os-folder-status-bar {
635 border-top: 1px solid var( --os-ui-border, #dcdcde );
636 background: transparent;
637 color: var( --os-ui-fg-muted, #50575e );
638 }
639
640 .desktop-mode-folder-window
641 .os-folder-status-bar
642 button.os-folder-status-bar__segment:hover {
643 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
644 }
645
646 /* Generic breadcrumb header — any surface that grows a navigation
647 * stack (folder window, My WordPress folder, future drill-down
648 * windows) renders one of these via `renderBreadcrumbs` from
649 * `src/desktop-files/breadcrumbs.ts`. Visual vocabulary: small
650 * icon-only Back button on the left, `›`-separated crumb buttons,
651 * current segment is bold + non-interactive.
652 *
653 * @since 0.8.0
654 */
655 .os-breadcrumbs {
656 display: flex;
657 align-items: center;
658 gap: 8px;
659 padding: 6px 10px;
660 border-bottom: 1px solid var( --os-ui-border, #dcdcde );
661 background: transparent;
662 flex: 0 0 auto;
663 }
664
665 .os-breadcrumbs__back {
666 display: inline-flex;
667 align-items: center;
668 justify-content: center;
669 width: 26px;
670 height: 26px;
671 padding: 0;
672 border: 0;
673 background: transparent;
674 border-radius: 4px;
675 cursor: pointer;
676 color: inherit;
677 flex: 0 0 auto;
678 }
679
680 .os-breadcrumbs__back:hover:not(:disabled),
681 .os-breadcrumbs__back:focus-visible:not(:disabled) {
682 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
683 outline: none;
684 }
685
686 .os-breadcrumbs__back:disabled {
687 opacity: 0.35;
688 cursor: default;
689 }
690
691 .os-breadcrumbs__back .dashicons {
692 font-size: 18px;
693 width: 18px;
694 height: 18px;
695 line-height: 1;
696 }
697
698 .os-breadcrumbs__crumbs {
699 display: flex;
700 align-items: center;
701 gap: 6px;
702 font-size: 13px;
703 min-width: 0;
704 overflow: hidden;
705 text-overflow: ellipsis;
706 white-space: nowrap;
707 }
708
709 .os-breadcrumbs__crumb {
710 border: 0;
711 background: transparent;
712 color: var( --os-link, var( --os-ui-accent, #2271b1 ) );
713 cursor: pointer;
714 padding: 2px 4px;
715 border-radius: 4px;
716 font: inherit;
717 }
718
719 .os-breadcrumbs__crumb:hover {
720 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
721 }
722
723 .os-breadcrumbs__crumb--current {
724 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
725 cursor: default;
726 font-weight: 600;
727 }
728
729 .os-breadcrumbs__crumb--current:hover {
730 background: transparent;
731 }
732
733 .os-breadcrumbs__sep {
734 color: var( --os-ui-fg-muted, #787c82 );
735 }
736
737 .os-folder-window__layer {
738 position: relative;
739 flex: 1;
740 min-height: 0;
741 overflow: auto;
742 }
743
744 /* Two-pane shell — left: tile canvas, right: preview pane that
745 * reflects the currently-selected tile. Same model as the My
746 * WordPress two-pane view so the UX is unified.
747 *
748 * @since 0.8.0 */
749 .os-folder-window__split {
750 display: grid;
751 grid-template-columns: minmax( 240px, 60% ) minmax( 0, 1fr );
752 flex: 1 1 auto;
753 min-height: 0;
754 }
755
756 .os-folder-window__preview {
757 overflow-y: auto;
758 min-width: 0;
759 background: var( --os-window-bg, #fff );
760 border-inline-start: 1px solid var( --os-ui-border, #dcdcde );
761 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
762 }
763
764 /* Selected file tile — light-blue accent on the wallpaper, soft
765 * accent inside light-context windows. Variable-driven so plugins
766 * can retune. */
767 .os-file-tile--selected,
768 .os-file-tile--selected:hover {
769 background: var(
770 --os-tile-selected-bg,
771 rgba( 34, 113, 177, 0.18 )
772 );
773 box-shadow: inset 0 0 0 1px
774 var( --os-tile-focus-ring, var( --wp-admin-theme-color, #2271b1 ) );
775 }
776
777 /* Inside a folder window the selected highlight should read on
778 * white. */
779 .desktop-mode-folder-window {
780 --os-tile-selected-bg: rgba( 34, 113, 177, 0.12 );
781 }
782
783 /* Type breakdown under a multi-selection's count in a folder
784 * window's preview pane. The count is the headline; this is the
785 * detail that tells you which actions the menu will offer. */
786 .os-files-preview__selection-breakdown {
787 font-size: 12px;
788 opacity: 0.75;
789 }
790
791 /* While a rubber band is live, nothing in the shell is selectable.
792 *
793 * The band starts on bare canvas, which — unlike a tile — IS
794 * selectable, so the browser begins its own text selection on the
795 * same press and keeps extending it as the pointer travels. Inside
796 * the canvas that stays invisible; drag out over another window and
797 * it starts highlighting that window's text in blue, mid-gesture.
798 *
799 * The controller also refuses `selectstart` outright while the band
800 * is up. This rule is the second half: it covers anything already
801 * selectable that the event route misses. */
802 body[ data-os-marquee ] {
803 -webkit-user-select: none;
804 user-select: none;
805 }
806
807 /* Marquee (rubber-band) selection box. Painted by the shared
808 * selection controller on any canvas that opts into it — the
809 * wallpaper, folder windows, every My WordPress list — so this one
810 * declaration dresses all of them.
811 *
812 * `pointer-events: none` is load-bearing: the box tracks under the
813 * cursor for the whole gesture, and without it every hit-test
814 * (drop targets, the controller's own tile lookup) would land on
815 * the box instead of what's beneath it. */
816 .os-selection-marquee {
817 position: absolute;
818 z-index: 5;
819 pointer-events: none;
820 border: 1px solid
821 var( --os-tile-focus-ring, var( --wp-admin-theme-color, #2271b1 ) );
822 border-radius: 2px;
823 /* A WASH, not a fill — the whole point of a rubber band is that
824 * you can see what it is about to catch. `--os-ui-accent-dim` is a
825 * solid hex (Pulse, one step back), so reaching for it directly
826 * paints an opaque rectangle over the icons; `--os-ui-accent-soft`
827 * is that same colour already taken down to a 14% wash, which is
828 * what every other ambient accent surface in the shell uses. */
829 background: var(
830 --os-selection-marquee-bg,
831 var( --os-ui-accent-soft, rgba( 34, 113, 177, 0.14 ) )
832 );
833 }
834
835 .os-folder-status-bar {
836 display: flex;
837 justify-content: space-between;
838 align-items: center;
839 gap: 12px;
840 padding: 6px 12px;
841 border-top: 1px solid rgba( 255, 255, 255, 0.08 );
842 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.18 ) );
843 color: var( --os-fg-muted, rgba( 255, 255, 255, 0.7 ) );
844 font-size: 12px;
845 flex-shrink: 0;
846 }
847
848 .os-folder-status-bar__cluster {
849 display: flex;
850 align-items: center;
851 gap: 12px;
852 }
853
854 .os-folder-status-bar__segment {
855 display: inline-flex;
856 align-items: center;
857 gap: 4px;
858 padding: 0;
859 border: 0;
860 background: transparent;
861 color: inherit;
862 font: inherit;
863 }
864
865 button.os-folder-status-bar__segment {
866 cursor: pointer;
867 border-radius: 4px;
868 padding: 2px 6px;
869 }
870
871 button.os-folder-status-bar__segment:hover {
872 background: rgba( 255, 255, 255, 0.1 );
873 }
874
875 .os-folder-status-bar__icon {
876 font-size: 14px;
877 }
878
879 /*
880 * File Associations OS Settings tab (Phase 5). One row per file
881 * type: label on the left, a `<os-select>` of compatible
882 * openers on the right. Color tokens use the OS-Settings-canonical
883 * `--os-ui-fg-muted` so the tab inherits theme colors instead of
884 * falling back to white-on-light invisibility.
885 */
886 .os-file-associations__intro {
887 display: block;
888 margin: 0 0 16px;
889 color: var( --os-ui-fg-muted, #50575e );
890 font-size: 13px;
891 line-height: 1.5;
892 }
893
894 .os-file-associations__list {
895 display: flex;
896 flex-direction: column;
897 gap: 8px;
898 }
899
900 .os-file-associations__row {
901 display: flex;
902 align-items: center;
903 justify-content: space-between;
904 gap: 16px;
905 padding: 8px 12px;
906 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.04 ) );
907 border-radius: 6px;
908 }
909
910 .os-file-associations__label {
911 font-weight: 500;
912 flex: 1;
913 }
914
915 .os-file-associations__select {
916 min-width: 220px;
917 }
918
919 .os-file-associations__none,
920 .os-file-associations__empty {
921 color: var( --os-ui-fg-muted, #50575e );
922 font-size: 12px;
923 }
924
925 /*
926 * The shell's two built-in modals — "New folder" / "Rename", and the
927 * web-link dialog, which reuses this class set for its surface. Both
928 * are light-DOM overlays that slot `<os-text-field>` +
929 * `<os-button>` for their controls.
930 *
931 * The surface reads the same `--os-ui-modal-*` family that
932 * `<os-modal>` and `<os-confirm-dialog>` do, so the three look like
933 * one dialog system and answer to a desktop theme together. Two
934 * things it must NOT do, both of which it used to:
935 *
936 * - Paint from `--os-bg`. That is the WALLPAPER token: the desk
937 * default is a gradient and the wallpaper layer overwrites it
938 * with whatever artwork is active. A dialog is a surface, not a
939 * desk. Same note as `os-modal.styles.ts`.
940 * - Style raw form controls. Core's `forms.css` reaches every
941 * `<input>` in the parent shell through `input[type="text"]` —
942 * (0,1,1), which outranks a single class of ours — so a plain
943 * input rendered as a white core-chrome box on this dark
944 * surface no matter what we declared. The controls live in
945 * shadow DOM now, where that sheet cannot follow.
946 */
947 .os-create-folder-dialog__overlay {
948 position: fixed;
949 inset: 0;
950 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.45 ) );
951 /* Desktop-theme texture slot: unset resolves to none. Mirrors
952 the scrim treatment in os-modal. */
953 background-image: var( --os-ui-scrim-image, none );
954 background-repeat: var( --os-ui-scrim-image-repeat, repeat );
955 background-size: var( --os-ui-scrim-image-size, auto );
956 background-position: var( --os-ui-scrim-image-position, center );
957 backdrop-filter: blur( 2px );
958 display: flex;
959 align-items: center;
960 justify-content: center;
961 z-index: 10000;
962 }
963
964 .os-create-folder-dialog {
965 /*
966 * Re-point the shared control tokens for a dark dialog surface,
967 * exactly as `<os-modal>` does on its host. The slotted
968 * `<os-text-field>` / `<os-button>` inherit these across the
969 * shadow boundary, so the field resolves a dark input with light
970 * ink instead of the light-admin defaults (`--os-window-bg` is
971 * `#fff` in an unthemed shell, which under a light `--os-ui-fg`
972 * is the invisible-value trap the modal styles call out).
973 *
974 * Each reads a palette-owned `--os-ui-modal-*` name first, so a
975 * desktop theme can still reach every one of them; the literals
976 * are only the floor for a shell whose stylesheet never loaded.
977 */
978 --os-ui-fg: var( --os-ui-modal-text, #f0f0f1 );
979 --os-ui-fg-muted: var( --os-ui-modal-text-muted, #a7aaad );
980 --os-ui-border: var( --os-ui-modal-border, rgba( 255, 255, 255, 0.25 ) );
981 --os-window-bg: var( --os-ui-modal-field-bg, #2c3338 );
982 --os-ui-button-bg-hover: var(
983 --os-ui-modal-button-bg-hover,
984 rgba( 255, 255, 255, 0.08 )
985 );
986
987 width: min( 420px, 92vw );
988 /* Longhand, and a literal fallback: the shorthand would reset the
989 texture slot below, and a gradient is invalid as a
990 background-color — it would leave the dialog transparent. */
991 background-color: var( --os-ui-modal-bg, #1d2327 );
992 background-image: var( --os-ui-dialog-bg-image, none );
993 background-repeat: var( --os-ui-dialog-bg-image-repeat, repeat );
994 background-size: var( --os-ui-dialog-bg-image-size, auto );
995 background-position: var( --os-ui-dialog-bg-image-position, center );
996 color: var( --os-ui-modal-fg, var( --os-fg, #fff ) );
997 border: 1px solid var( --os-ui-border, rgba( 255, 255, 255, 0.08 ) );
998 border-radius: 10px;
999 box-shadow: 0 20px 50px rgba( 0, 0, 0, 0.6 );
1000 padding: 20px 22px 18px;
1001 display: flex;
1002 flex-direction: column;
1003 gap: 10px;
1004 }
1005
1006 .os-create-folder-dialog__title {
1007 margin: 0 0 4px;
1008 font-size: 16px;
1009 font-weight: 600;
1010 color: var( --os-ui-fg, #f0f0f1 );
1011 }
1012
1013 .os-url-dialog__description {
1014 margin: 0 0 4px;
1015 font-size: 12px;
1016 line-height: 1.5;
1017 color: var( --os-ui-fg-muted, #a7aaad );
1018 }
1019
1020 .os-create-folder-dialog__error {
1021 margin: 0;
1022 color: var( --os-ui-danger-hover, #ff8a8a );
1023 font-size: 12px;
1024 }
1025
1026 .os-create-folder-dialog__actions {
1027 display: flex;
1028 justify-content: flex-end;
1029 gap: 8px;
1030 margin-top: 6px;
1031 }
1032
1033 /* The busy state dims the whole form area rather than the input
1034 alone — the buttons carry their own disabled treatment from
1035 os-button, and dimming one control of three read as a glitch. */
1036 .os-create-folder-dialog--busy .os-create-folder-dialog__field {
1037 opacity: 0.7;
1038 }
1039
1040 /*
1041 * Single label rule for every tile, regardless of host surface.
1042 * Visual properties read from `--os-tile-label-*`
1043 * tokens — defined in `variables.css` for the dark wallpaper and
1044 * rebound by any light-context scope (folder window, My
1045 * WordPress, …) so all surfaces share one source of truth.
1046 */
1047 .os-file-tile__label {
1048 font-size: 12px;
1049 line-height: 1.2;
1050 /* Follows the tile rather than restating its width, so a section
1051 * that re-points `--os-tile-w` (image-led sections do) gets a
1052 * label that grows with the tile instead of staying narrow. */
1053 max-width: calc( var( --os-tile-w, 88px ) - 4px );
1054 text-align: center;
1055 overflow: hidden;
1056 text-overflow: ellipsis;
1057 display: -webkit-box;
1058 -webkit-line-clamp: 2;
1059 -webkit-box-orient: vertical;
1060 color: var( --os-tile-label-color, var( --os-tile-fg, #fff ) );
1061 font-weight: var( --os-tile-label-weight, 400 );
1062 text-shadow: var( --os-tile-label-shadow, 0 1px 2px rgba( 0, 0, 0, 0.5 ) );
1063 -webkit-font-smoothing: var( --os-tile-label-smoothing, auto );
1064 -moz-osx-font-smoothing: grayscale;
1065 }
1066
1067 /*
1068 * DragManager ghost element (the visual that follows the cursor
1069 * during a drag). Cloned from the source by `mountGhost()` and
1070 * pinned to the body with `position: fixed` + transform-based
1071 * movement. `pointer-events: none` ensures the ghost itself
1072 * never obscures the drop targets underneath during hit-testing.
1073 *
1074 * The accept / reject classes are toggled on each hover transition
1075 * so the cursor matches the drop semantics: `copy` over an
1076 * accepting target, `no-drop` over rejecting (or no) target.
1077 *
1078 * @since 0.18.0
1079 */
1080 .os-drag-ghost {
1081 box-shadow: 0 6px 24px rgba( 0, 0, 0, 0.45 );
1082 border-radius: 6px;
1083 opacity: 0.95;
1084 transition: none;
1085 }
1086
1087 .os-drag-ghost--accept {
1088 cursor: copy;
1089 }
1090
1091 .os-drag-ghost--reject {
1092 cursor: no-drop;
1093 }
1094
1095 /* Multi-item drag ghost: the grabbed tile, with the rest of the set
1096 * implied by two offset cards behind it and stated by a count badge.
1097 * The stack is drawn with pseudo-elements rather than real clones —
1098 * three cloned tiles cost three subtree copies per lift, and only
1099 * the top one is ever legible. */
1100 .os-drag-stack {
1101 /* The drag manager overwrites this with `position: fixed` when it
1102 * mounts the ghost. It stays declared so the stack still composes
1103 * correctly if the helper is used outside a live drag — both
1104 * values establish the containing block the cards and the badge
1105 * are placed against. */
1106 position: relative;
1107 }
1108
1109 .os-drag-stack::before,
1110 .os-drag-stack::after {
1111 content: '';
1112 position: absolute;
1113 inset: 0;
1114 z-index: -1;
1115 border-radius: 6px;
1116 background: var( --os-ui-surface, rgba( 30, 30, 40, 0.9 ) );
1117 box-shadow: 0 4px 14px rgba( 0, 0, 0, 0.35 );
1118 }
1119
1120 .os-drag-stack::before {
1121 transform: translate( 6px, 6px );
1122 opacity: 0.7;
1123 }
1124
1125 .os-drag-stack::after {
1126 transform: translate( 12px, 12px );
1127 opacity: 0.45;
1128 }
1129
1130 .os-drag-stack__count {
1131 position: absolute;
1132 inset-block-start: -8px;
1133 inset-inline-end: -8px;
1134 z-index: 1;
1135 min-width: 20px;
1136 height: 20px;
1137 padding: 0 6px;
1138 box-sizing: border-box;
1139 display: flex;
1140 align-items: center;
1141 justify-content: center;
1142 border-radius: 10px;
1143 background: var(
1144 --os-ui-accent,
1145 var( --wp-admin-theme-color, #2271b1 )
1146 );
1147 color: #fff;
1148 font-size: 11px;
1149 font-weight: 600;
1150 line-height: 1;
1151 box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.4 );
1152 }
1153
1154 /* ============================================================ *
1155 * Upload progress HUD (since 0.31.0)
1156 *
1157 * Pinned floating panel that surfaces the OS-file-drop manager's
1158 * in-flight uploads. One row per file with a `<os-progress-bar>`,
1159 * a filename + status, and a Cancel/Dismiss action. The panel
1160 * hides itself when there are no rows; it's purely reactive to
1161 * the four upload-lifecycle hooks.
1162 * ============================================================ */
1163
1164 .os-upload-hud {
1165 position: fixed;
1166 inset-block-end: 16px;
1167 inset-inline-end: 16px;
1168 width: 320px;
1169 max-width: calc( 100vw - 32px );
1170 background: var( --os-panel-bg, var( --os-ui-surface, #fff ) );
1171 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
1172 border: 1px solid var( --os-ui-border, #dcdcde );
1173 border-radius: 10px;
1174 box-shadow: 0 8px 28px rgba( 0, 0, 0, 0.18 );
1175 z-index: 99998;
1176 font: inherit;
1177 pointer-events: auto;
1178 overflow: hidden;
1179 }
1180
1181 .os-upload-hud[ hidden ] {
1182 display: none;
1183 }
1184
1185 .os-upload-hud__header {
1186 display: flex;
1187 align-items: center;
1188 justify-content: space-between;
1189 gap: 8px;
1190 padding: 8px 12px;
1191 background: var( --os-panel-header-bg, var( --os-ui-hover, rgba( 0, 0, 0, 0.04 ) ) );
1192 border-block-end: 1px solid var( --os-ui-border, #dcdcde );
1193 font-weight: 600;
1194 font-size: 12px;
1195 }
1196
1197 .os-upload-hud__title {
1198 min-width: 0;
1199 overflow: hidden;
1200 text-overflow: ellipsis;
1201 white-space: nowrap;
1202 }
1203
1204 .os-upload-hud__close {
1205 border: 0;
1206 background: transparent;
1207 color: inherit;
1208 font-size: 16px;
1209 line-height: 1;
1210 width: 22px;
1211 height: 22px;
1212 border-radius: 4px;
1213 cursor: pointer;
1214 display: inline-flex;
1215 align-items: center;
1216 justify-content: center;
1217 }
1218 .os-upload-hud__close:hover,
1219 .os-upload-hud__close:focus-visible {
1220 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.08 ) );
1221 outline: none;
1222 }
1223
1224 .os-upload-hud__list {
1225 display: flex;
1226 flex-direction: column;
1227 gap: 10px;
1228 padding: 10px 12px;
1229 max-height: 50vh;
1230 overflow-y: auto;
1231 }
1232
1233 .os-upload-hud__row {
1234 display: grid;
1235 grid-template-columns: 1fr auto;
1236 grid-template-areas:
1237 'meta actions'
1238 'bar bar';
1239 gap: 6px 8px;
1240 align-items: center;
1241 font-size: 12px;
1242 }
1243
1244 .os-upload-hud__meta {
1245 grid-area: meta;
1246 min-width: 0;
1247 display: flex;
1248 flex-direction: column;
1249 gap: 2px;
1250 }
1251
1252 .os-upload-hud__name {
1253 overflow: hidden;
1254 text-overflow: ellipsis;
1255 white-space: nowrap;
1256 font-weight: 500;
1257 }
1258
1259 .os-upload-hud__status {
1260 font-size: 11px;
1261 opacity: 0.75;
1262 font-variant-numeric: tabular-nums;
1263 }
1264
1265 .os-upload-hud__row os-progress-bar {
1266 grid-area: bar;
1267 }
1268
1269 .os-upload-hud__actions {
1270 grid-area: actions;
1271 display: inline-flex;
1272 gap: 4px;
1273 }
1274