PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.0
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.0.0, at assets/css/desktop-files.css

1,266 lines 36.0 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 --os-tile-fg-muted: rgba( 0, 0, 0, 0.55 );
611 --os-tile-hover-bg: rgba( 0, 0, 0, 0.06 );
612 /* Light-context label rendering — share the same recipe with
613 * every other light-bg tile surface (My WordPress, future
614 * windows). One set of tokens, no per-surface duplication. */
615 --os-tile-label-shadow: none;
616 --os-tile-label-weight: 500;
617 --os-tile-label-smoothing: antialiased;
618 --os-tile-label-color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
619 }
620
621 /* Folder window's bottom status bar — paint with the same light-
622 * context treatment My WordPress uses (transparent bg, soft border,
623 * muted dark text). Without this, the wallpaper-targeted defaults
624 * (`background: rgba(0,0,0,0.18); color: white`) would show as a
625 * dark band against the white window bg. */
626 .desktop-mode-folder-window .os-folder-status-bar {
627 border-top: 1px solid var( --os-ui-border, #dcdcde );
628 background: transparent;
629 color: var( --os-ui-fg-muted, #50575e );
630 }
631
632 .desktop-mode-folder-window
633 .os-folder-status-bar
634 button.os-folder-status-bar__segment:hover {
635 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
636 }
637
638 /* Generic breadcrumb header — any surface that grows a navigation
639 * stack (folder window, My WordPress folder, future drill-down
640 * windows) renders one of these via `renderBreadcrumbs` from
641 * `src/desktop-files/breadcrumbs.ts`. Visual vocabulary: small
642 * icon-only Back button on the left, `›`-separated crumb buttons,
643 * current segment is bold + non-interactive.
644 *
645 * @since 0.8.0
646 */
647 .os-breadcrumbs {
648 display: flex;
649 align-items: center;
650 gap: 8px;
651 padding: 6px 10px;
652 border-bottom: 1px solid var( --os-ui-border, #dcdcde );
653 background: transparent;
654 flex: 0 0 auto;
655 }
656
657 .os-breadcrumbs__back {
658 display: inline-flex;
659 align-items: center;
660 justify-content: center;
661 width: 26px;
662 height: 26px;
663 padding: 0;
664 border: 0;
665 background: transparent;
666 border-radius: 4px;
667 cursor: pointer;
668 color: inherit;
669 flex: 0 0 auto;
670 }
671
672 .os-breadcrumbs__back:hover:not(:disabled),
673 .os-breadcrumbs__back:focus-visible:not(:disabled) {
674 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
675 outline: none;
676 }
677
678 .os-breadcrumbs__back:disabled {
679 opacity: 0.35;
680 cursor: default;
681 }
682
683 .os-breadcrumbs__back .dashicons {
684 font-size: 18px;
685 width: 18px;
686 height: 18px;
687 line-height: 1;
688 }
689
690 .os-breadcrumbs__crumbs {
691 display: flex;
692 align-items: center;
693 gap: 6px;
694 font-size: 13px;
695 min-width: 0;
696 overflow: hidden;
697 text-overflow: ellipsis;
698 white-space: nowrap;
699 }
700
701 .os-breadcrumbs__crumb {
702 border: 0;
703 background: transparent;
704 color: var( --os-link, var( --os-ui-accent, #2271b1 ) );
705 cursor: pointer;
706 padding: 2px 4px;
707 border-radius: 4px;
708 font: inherit;
709 }
710
711 .os-breadcrumbs__crumb:hover {
712 background: var( --os-hover, var( --os-ui-hover, rgba( 0, 0, 0, 0.06 ) ) );
713 }
714
715 .os-breadcrumbs__crumb--current {
716 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
717 cursor: default;
718 font-weight: 600;
719 }
720
721 .os-breadcrumbs__crumb--current:hover {
722 background: transparent;
723 }
724
725 .os-breadcrumbs__sep {
726 color: var( --os-ui-fg-muted, #787c82 );
727 }
728
729 .os-folder-window__layer {
730 position: relative;
731 flex: 1;
732 min-height: 0;
733 overflow: auto;
734 }
735
736 /* Two-pane shell — left: tile canvas, right: preview pane that
737 * reflects the currently-selected tile. Same model as the My
738 * WordPress two-pane view so the UX is unified.
739 *
740 * @since 0.8.0 */
741 .os-folder-window__split {
742 display: grid;
743 grid-template-columns: minmax( 240px, 60% ) minmax( 0, 1fr );
744 flex: 1 1 auto;
745 min-height: 0;
746 }
747
748 .os-folder-window__preview {
749 overflow-y: auto;
750 min-width: 0;
751 background: var( --os-window-bg, #fff );
752 border-inline-start: 1px solid var( --os-ui-border, #dcdcde );
753 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
754 }
755
756 /* Selected file tile — light-blue accent on the wallpaper, soft
757 * accent inside light-context windows. Variable-driven so plugins
758 * can retune. */
759 .os-file-tile--selected,
760 .os-file-tile--selected:hover {
761 background: var(
762 --os-tile-selected-bg,
763 rgba( 34, 113, 177, 0.18 )
764 );
765 box-shadow: inset 0 0 0 1px
766 var( --os-tile-focus-ring, var( --wp-admin-theme-color, #2271b1 ) );
767 }
768
769 /* Inside a folder window the selected highlight should read on
770 * white. */
771 .desktop-mode-folder-window {
772 --os-tile-selected-bg: rgba( 34, 113, 177, 0.12 );
773 }
774
775 /* Type breakdown under a multi-selection's count in a folder
776 * window's preview pane. The count is the headline; this is the
777 * detail that tells you which actions the menu will offer. */
778 .os-files-preview__selection-breakdown {
779 font-size: 12px;
780 opacity: 0.75;
781 }
782
783 /* While a rubber band is live, nothing in the shell is selectable.
784 *
785 * The band starts on bare canvas, which — unlike a tile — IS
786 * selectable, so the browser begins its own text selection on the
787 * same press and keeps extending it as the pointer travels. Inside
788 * the canvas that stays invisible; drag out over another window and
789 * it starts highlighting that window's text in blue, mid-gesture.
790 *
791 * The controller also refuses `selectstart` outright while the band
792 * is up. This rule is the second half: it covers anything already
793 * selectable that the event route misses. */
794 body[ data-os-marquee ] {
795 -webkit-user-select: none;
796 user-select: none;
797 }
798
799 /* Marquee (rubber-band) selection box. Painted by the shared
800 * selection controller on any canvas that opts into it — the
801 * wallpaper, folder windows, every My WordPress list — so this one
802 * declaration dresses all of them.
803 *
804 * `pointer-events: none` is load-bearing: the box tracks under the
805 * cursor for the whole gesture, and without it every hit-test
806 * (drop targets, the controller's own tile lookup) would land on
807 * the box instead of what's beneath it. */
808 .os-selection-marquee {
809 position: absolute;
810 z-index: 5;
811 pointer-events: none;
812 border: 1px solid
813 var( --os-tile-focus-ring, var( --wp-admin-theme-color, #2271b1 ) );
814 border-radius: 2px;
815 /* A WASH, not a fill — the whole point of a rubber band is that
816 * you can see what it is about to catch. `--os-ui-accent-dim` is a
817 * solid hex (Pulse, one step back), so reaching for it directly
818 * paints an opaque rectangle over the icons; `--os-ui-accent-soft`
819 * is that same colour already taken down to a 14% wash, which is
820 * what every other ambient accent surface in the shell uses. */
821 background: var(
822 --os-selection-marquee-bg,
823 var( --os-ui-accent-soft, rgba( 34, 113, 177, 0.14 ) )
824 );
825 }
826
827 .os-folder-status-bar {
828 display: flex;
829 justify-content: space-between;
830 align-items: center;
831 gap: 12px;
832 padding: 6px 12px;
833 border-top: 1px solid rgba( 255, 255, 255, 0.08 );
834 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.18 ) );
835 color: var( --os-fg-muted, rgba( 255, 255, 255, 0.7 ) );
836 font-size: 12px;
837 flex-shrink: 0;
838 }
839
840 .os-folder-status-bar__cluster {
841 display: flex;
842 align-items: center;
843 gap: 12px;
844 }
845
846 .os-folder-status-bar__segment {
847 display: inline-flex;
848 align-items: center;
849 gap: 4px;
850 padding: 0;
851 border: 0;
852 background: transparent;
853 color: inherit;
854 font: inherit;
855 }
856
857 button.os-folder-status-bar__segment {
858 cursor: pointer;
859 border-radius: 4px;
860 padding: 2px 6px;
861 }
862
863 button.os-folder-status-bar__segment:hover {
864 background: rgba( 255, 255, 255, 0.1 );
865 }
866
867 .os-folder-status-bar__icon {
868 font-size: 14px;
869 }
870
871 /*
872 * File Associations OS Settings tab (Phase 5). One row per file
873 * type: label on the left, a `<os-select>` of compatible
874 * openers on the right. Color tokens use the OS-Settings-canonical
875 * `--os-ui-fg-muted` so the tab inherits theme colors instead of
876 * falling back to white-on-light invisibility.
877 */
878 .os-file-associations__intro {
879 display: block;
880 margin: 0 0 16px;
881 color: var( --os-ui-fg-muted, #50575e );
882 font-size: 13px;
883 line-height: 1.5;
884 }
885
886 .os-file-associations__list {
887 display: flex;
888 flex-direction: column;
889 gap: 8px;
890 }
891
892 .os-file-associations__row {
893 display: flex;
894 align-items: center;
895 justify-content: space-between;
896 gap: 16px;
897 padding: 8px 12px;
898 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.04 ) );
899 border-radius: 6px;
900 }
901
902 .os-file-associations__label {
903 font-weight: 500;
904 flex: 1;
905 }
906
907 .os-file-associations__select {
908 min-width: 220px;
909 }
910
911 .os-file-associations__none,
912 .os-file-associations__empty {
913 color: var( --os-ui-fg-muted, #50575e );
914 font-size: 12px;
915 }
916
917 /*
918 * The shell's two built-in modals — "New folder" / "Rename", and the
919 * web-link dialog, which reuses this class set for its surface. Both
920 * are light-DOM overlays that slot `<os-text-field>` +
921 * `<os-button>` for their controls.
922 *
923 * The surface reads the same `--os-ui-modal-*` family that
924 * `<os-modal>` and `<os-confirm-dialog>` do, so the three look like
925 * one dialog system and answer to a desktop theme together. Two
926 * things it must NOT do, both of which it used to:
927 *
928 * - Paint from `--os-bg`. That is the WALLPAPER token: the desk
929 * default is a gradient and the wallpaper layer overwrites it
930 * with whatever artwork is active. A dialog is a surface, not a
931 * desk. Same note as `os-modal.styles.ts`.
932 * - Style raw form controls. Core's `forms.css` reaches every
933 * `<input>` in the parent shell through `input[type="text"]` —
934 * (0,1,1), which outranks a single class of ours — so a plain
935 * input rendered as a white core-chrome box on this dark
936 * surface no matter what we declared. The controls live in
937 * shadow DOM now, where that sheet cannot follow.
938 */
939 .os-create-folder-dialog__overlay {
940 position: fixed;
941 inset: 0;
942 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.45 ) );
943 /* Desktop-theme texture slot: unset resolves to none. Mirrors
944 the scrim treatment in os-modal. */
945 background-image: var( --os-ui-scrim-image, none );
946 background-repeat: var( --os-ui-scrim-image-repeat, repeat );
947 background-size: var( --os-ui-scrim-image-size, auto );
948 background-position: var( --os-ui-scrim-image-position, center );
949 backdrop-filter: blur( 2px );
950 display: flex;
951 align-items: center;
952 justify-content: center;
953 z-index: 10000;
954 }
955
956 .os-create-folder-dialog {
957 /*
958 * Re-point the shared control tokens for a dark dialog surface,
959 * exactly as `<os-modal>` does on its host. The slotted
960 * `<os-text-field>` / `<os-button>` inherit these across the
961 * shadow boundary, so the field resolves a dark input with light
962 * ink instead of the light-admin defaults (`--os-window-bg` is
963 * `#fff` in an unthemed shell, which under a light `--os-ui-fg`
964 * is the invisible-value trap the modal styles call out).
965 *
966 * Each reads a palette-owned `--os-ui-modal-*` name first, so a
967 * desktop theme can still reach every one of them; the literals
968 * are only the floor for a shell whose stylesheet never loaded.
969 */
970 --os-ui-fg: var( --os-ui-modal-text, #f0f0f1 );
971 --os-ui-fg-muted: var( --os-ui-modal-text-muted, #a7aaad );
972 --os-ui-border: var( --os-ui-modal-border, rgba( 255, 255, 255, 0.25 ) );
973 --os-window-bg: var( --os-ui-modal-field-bg, #2c3338 );
974 --os-ui-button-bg-hover: var(
975 --os-ui-modal-button-bg-hover,
976 rgba( 255, 255, 255, 0.08 )
977 );
978
979 width: min( 420px, 92vw );
980 /* Longhand, and a literal fallback: the shorthand would reset the
981 texture slot below, and a gradient is invalid as a
982 background-color — it would leave the dialog transparent. */
983 background-color: var( --os-ui-modal-bg, #1d2327 );
984 background-image: var( --os-ui-dialog-bg-image, none );
985 background-repeat: var( --os-ui-dialog-bg-image-repeat, repeat );
986 background-size: var( --os-ui-dialog-bg-image-size, auto );
987 background-position: var( --os-ui-dialog-bg-image-position, center );
988 color: var( --os-ui-modal-fg, var( --os-fg, #fff ) );
989 border: 1px solid var( --os-ui-border, rgba( 255, 255, 255, 0.08 ) );
990 border-radius: 10px;
991 box-shadow: 0 20px 50px rgba( 0, 0, 0, 0.6 );
992 padding: 20px 22px 18px;
993 display: flex;
994 flex-direction: column;
995 gap: 10px;
996 }
997
998 .os-create-folder-dialog__title {
999 margin: 0 0 4px;
1000 font-size: 16px;
1001 font-weight: 600;
1002 color: var( --os-ui-fg, #f0f0f1 );
1003 }
1004
1005 .os-url-dialog__description {
1006 margin: 0 0 4px;
1007 font-size: 12px;
1008 line-height: 1.5;
1009 color: var( --os-ui-fg-muted, #a7aaad );
1010 }
1011
1012 .os-create-folder-dialog__error {
1013 margin: 0;
1014 color: var( --os-ui-danger-hover, #ff8a8a );
1015 font-size: 12px;
1016 }
1017
1018 .os-create-folder-dialog__actions {
1019 display: flex;
1020 justify-content: flex-end;
1021 gap: 8px;
1022 margin-top: 6px;
1023 }
1024
1025 /* The busy state dims the whole form area rather than the input
1026 alone — the buttons carry their own disabled treatment from
1027 os-button, and dimming one control of three read as a glitch. */
1028 .os-create-folder-dialog--busy .os-create-folder-dialog__field {
1029 opacity: 0.7;
1030 }
1031
1032 /*
1033 * Single label rule for every tile, regardless of host surface.
1034 * Visual properties read from `--os-tile-label-*`
1035 * tokens — defined in `variables.css` for the dark wallpaper and
1036 * rebound by any light-context scope (folder window, My
1037 * WordPress, …) so all surfaces share one source of truth.
1038 */
1039 .os-file-tile__label {
1040 font-size: 12px;
1041 line-height: 1.2;
1042 /* Follows the tile rather than restating its width, so a section
1043 * that re-points `--os-tile-w` (image-led sections do) gets a
1044 * label that grows with the tile instead of staying narrow. */
1045 max-width: calc( var( --os-tile-w, 88px ) - 4px );
1046 text-align: center;
1047 overflow: hidden;
1048 text-overflow: ellipsis;
1049 display: -webkit-box;
1050 -webkit-line-clamp: 2;
1051 -webkit-box-orient: vertical;
1052 color: var( --os-tile-label-color, var( --os-tile-fg, #fff ) );
1053 font-weight: var( --os-tile-label-weight, 400 );
1054 text-shadow: var( --os-tile-label-shadow, 0 1px 2px rgba( 0, 0, 0, 0.5 ) );
1055 -webkit-font-smoothing: var( --os-tile-label-smoothing, auto );
1056 -moz-osx-font-smoothing: grayscale;
1057 }
1058
1059 /*
1060 * DragManager ghost element (the visual that follows the cursor
1061 * during a drag). Cloned from the source by `mountGhost()` and
1062 * pinned to the body with `position: fixed` + transform-based
1063 * movement. `pointer-events: none` ensures the ghost itself
1064 * never obscures the drop targets underneath during hit-testing.
1065 *
1066 * The accept / reject classes are toggled on each hover transition
1067 * so the cursor matches the drop semantics: `copy` over an
1068 * accepting target, `no-drop` over rejecting (or no) target.
1069 *
1070 * @since 0.18.0
1071 */
1072 .os-drag-ghost {
1073 box-shadow: 0 6px 24px rgba( 0, 0, 0, 0.45 );
1074 border-radius: 6px;
1075 opacity: 0.95;
1076 transition: none;
1077 }
1078
1079 .os-drag-ghost--accept {
1080 cursor: copy;
1081 }
1082
1083 .os-drag-ghost--reject {
1084 cursor: no-drop;
1085 }
1086
1087 /* Multi-item drag ghost: the grabbed tile, with the rest of the set
1088 * implied by two offset cards behind it and stated by a count badge.
1089 * The stack is drawn with pseudo-elements rather than real clones —
1090 * three cloned tiles cost three subtree copies per lift, and only
1091 * the top one is ever legible. */
1092 .os-drag-stack {
1093 /* The drag manager overwrites this with `position: fixed` when it
1094 * mounts the ghost. It stays declared so the stack still composes
1095 * correctly if the helper is used outside a live drag — both
1096 * values establish the containing block the cards and the badge
1097 * are placed against. */
1098 position: relative;
1099 }
1100
1101 .os-drag-stack::before,
1102 .os-drag-stack::after {
1103 content: '';
1104 position: absolute;
1105 inset: 0;
1106 z-index: -1;
1107 border-radius: 6px;
1108 background: var( --os-ui-surface, rgba( 30, 30, 40, 0.9 ) );
1109 box-shadow: 0 4px 14px rgba( 0, 0, 0, 0.35 );
1110 }
1111
1112 .os-drag-stack::before {
1113 transform: translate( 6px, 6px );
1114 opacity: 0.7;
1115 }
1116
1117 .os-drag-stack::after {
1118 transform: translate( 12px, 12px );
1119 opacity: 0.45;
1120 }
1121
1122 .os-drag-stack__count {
1123 position: absolute;
1124 inset-block-start: -8px;
1125 inset-inline-end: -8px;
1126 z-index: 1;
1127 min-width: 20px;
1128 height: 20px;
1129 padding: 0 6px;
1130 box-sizing: border-box;
1131 display: flex;
1132 align-items: center;
1133 justify-content: center;
1134 border-radius: 10px;
1135 background: var(
1136 --os-ui-accent,
1137 var( --wp-admin-theme-color, #2271b1 )
1138 );
1139 color: #fff;
1140 font-size: 11px;
1141 font-weight: 600;
1142 line-height: 1;
1143 box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.4 );
1144 }
1145
1146 /* ============================================================ *
1147 * Upload progress HUD (since 0.31.0)
1148 *
1149 * Pinned floating panel that surfaces the OS-file-drop manager's
1150 * in-flight uploads. One row per file with a `<os-progress-bar>`,
1151 * a filename + status, and a Cancel/Dismiss action. The panel
1152 * hides itself when there are no rows; it's purely reactive to
1153 * the four upload-lifecycle hooks.
1154 * ============================================================ */
1155
1156 .os-upload-hud {
1157 position: fixed;
1158 inset-block-end: 16px;
1159 inset-inline-end: 16px;
1160 width: 320px;
1161 max-width: calc( 100vw - 32px );
1162 background: var( --os-panel-bg, var( --os-ui-surface, #fff ) );
1163 color: var( --os-fg-on-light, var( --os-ui-fg, #1d2327 ) );
1164 border: 1px solid var( --os-ui-border, #dcdcde );
1165 border-radius: 10px;
1166 box-shadow: 0 8px 28px rgba( 0, 0, 0, 0.18 );
1167 z-index: 99998;
1168 font: inherit;
1169 pointer-events: auto;
1170 overflow: hidden;
1171 }
1172
1173 .os-upload-hud[ hidden ] {
1174 display: none;
1175 }
1176
1177 .os-upload-hud__header {
1178 display: flex;
1179 align-items: center;
1180 justify-content: space-between;
1181 gap: 8px;
1182 padding: 8px 12px;
1183 background: var( --os-panel-header-bg, var( --os-ui-hover, rgba( 0, 0, 0, 0.04 ) ) );
1184 border-block-end: 1px solid var( --os-ui-border, #dcdcde );
1185 font-weight: 600;
1186 font-size: 12px;
1187 }
1188
1189 .os-upload-hud__title {
1190 min-width: 0;
1191 overflow: hidden;
1192 text-overflow: ellipsis;
1193 white-space: nowrap;
1194 }
1195
1196 .os-upload-hud__close {
1197 border: 0;
1198 background: transparent;
1199 color: inherit;
1200 font-size: 16px;
1201 line-height: 1;
1202 width: 22px;
1203 height: 22px;
1204 border-radius: 4px;
1205 cursor: pointer;
1206 display: inline-flex;
1207 align-items: center;
1208 justify-content: center;
1209 }
1210 .os-upload-hud__close:hover,
1211 .os-upload-hud__close:focus-visible {
1212 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.08 ) );
1213 outline: none;
1214 }
1215
1216 .os-upload-hud__list {
1217 display: flex;
1218 flex-direction: column;
1219 gap: 10px;
1220 padding: 10px 12px;
1221 max-height: 50vh;
1222 overflow-y: auto;
1223 }
1224
1225 .os-upload-hud__row {
1226 display: grid;
1227 grid-template-columns: 1fr auto;
1228 grid-template-areas:
1229 'meta actions'
1230 'bar bar';
1231 gap: 6px 8px;
1232 align-items: center;
1233 font-size: 12px;
1234 }
1235
1236 .os-upload-hud__meta {
1237 grid-area: meta;
1238 min-width: 0;
1239 display: flex;
1240 flex-direction: column;
1241 gap: 2px;
1242 }
1243
1244 .os-upload-hud__name {
1245 overflow: hidden;
1246 text-overflow: ellipsis;
1247 white-space: nowrap;
1248 font-weight: 500;
1249 }
1250
1251 .os-upload-hud__status {
1252 font-size: 11px;
1253 opacity: 0.75;
1254 font-variant-numeric: tabular-nums;
1255 }
1256
1257 .os-upload-hud__row os-progress-bar {
1258 grid-area: bar;
1259 }
1260
1261 .os-upload-hud__actions {
1262 grid-area: actions;
1263 display: inline-flex;
1264 gap: 4px;
1265 }
1266