PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / trunk
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin vtrunk
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 0.8.6 All 33 releases
desktop-mode / assets / css / desktop-files.css

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

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