/** * OpenStation — Dock Peek. * * Hover-reveal popover that fans out from a multi-instance dock tile. * Each card is styled as a miniature window — faux titlebar with * traffic-light dots + the page icon + the live window title, tinted * body with ghosted content lines. The trailing Ghost Card matches * the geometry but is dashed + breathing to announce itself as the * "open new" affordance. * * Click an instance card → `document.startViewTransition()` morphs * it into the focused window. Click the Ghost Card → same API * morphs it into the freshly-spawned window. * * Three motion layers: * 1. Tile lift — the dock icon springs up + magnifies on hover. * 2. Card fan-out — cards slide in staggered, with a subtle peel. * 3. Ghost pulse — the trailing card breathes until hovered. * * @since 0.6.2 */ /* ────────────────────────────────────────────────────────────────── Tile lift — applied to ANY dock tile on hover. Cheap and additive to the existing dock styles; doesn't depend on the peek being present (the icon should feel responsive even when there's no peek to show). ────────────────────────────────────────────────────────────────── */ .os-dock__item-primary { transition: transform 240ms cubic-bezier( 0.34, 1.56, 0.64, 1 ), filter 240ms ease-out; } /* Same two glyph/wash tokens as the base hover rule in dock.css — * this sheet re-states hover to add the lift and the drop shadow, and * it wins on specificity, so a theme that only reached the base rule * would see its colour vanish the moment the peek sheet loaded. * * `background-color`, not the `background` shorthand it used to be: * the shorthand also reset `background-image`, which erased the * DOCK_ITEM texture from under the cursor on any theme that ships * one. dock.css sets the resting wash as a colour for exactly this * reason — "so it composes with the texture rather than erasing it" — * and this rule quietly undid it on hover. */ .os-dock__item:hover .os-dock__item-primary, .os-dock__item[data-peek-active] .os-dock__item-primary { background-color: var( --os-dock-item-bg-hover, rgba( 255, 255, 255, 0.15 ) ); color: var( --os-dock-icon-color-hover, var( --os-ui-fg-on-accent, #fff ) ); transform: scale( 1.08 ); filter: drop-shadow( 0 4px 8px rgba( 0, 0, 0, 0.18 ) ); } @media ( prefers-reduced-motion: reduce ) { .os-dock__item-primary, .os-dock__item:hover .os-dock__item-primary, .os-dock__item[data-peek-active] .os-dock__item-primary { transition: none; transform: none; } } /* ────────────────────────────────────────────────────────────────── Popover surface — glass material, body-attached. ────────────────────────────────────────────────────────────────── */ .os-dock-peek { position: fixed; z-index: 999999; padding: 10px; border-radius: 16px; background: color-mix( in srgb, var( --os-bg, #1d2327 ) 76%, transparent ); backdrop-filter: blur( 28px ) saturate( 180% ); -webkit-backdrop-filter: blur( 28px ) saturate( 180% ); border: 1px solid rgba( 255, 255, 255, 0.12 ); box-shadow: 0 1px 0 rgba( 255, 255, 255, 0.08 ) inset, 0 16px 40px -10px rgba( 0, 0, 0, 0.55 ), 0 6px 16px -4px rgba( 0, 0, 0, 0.36 ); color: var( --os-fg, #fff ); pointer-events: auto; opacity: 0; display: flex; flex-direction: column; transition: opacity 160ms ease-out; /* Default (left/right docks) — vertical column. The popover caps in height; cards scroll inside. */ max-height: min( 80vh, 480px ); max-width: min( 90vw, 320px ); } /* Bottom-dock orientation: cards form a horizontal reel. The popover caps in WIDTH instead of height, and the cards container scrolls horizontally. Visually mirrors how taskbar-style docks work — a row of mini-windows above the icon, not a stack. */ .os-dock-peek[ data-orientation = "bottom" ] { max-height: none; max-width: min( 92vw, 920px ); } .os-dock-peek--open { opacity: 1; } /* Anchor flips by orientation. The popover's anchor point (the coordinate set in JS) sits ON the tile edge; we translate from there so the popover floats off-edge with a gap. The `--peek-clamp-x/y` props are added by JS when the popover would otherwise overflow the viewport — they nudge it inward without losing the orientation translate. Docks: top-aligned to the tile so the popover grows downward when minimized cards expand. The old vertical-centre anchor pushed the top edge into the browser's bar on growth. Bottom dock: stays above the tile, growing upward. */ .os-dock-peek[ data-orientation = "left" ] { transform: translate( var( --peek-clamp-x, 0 ), var( --peek-clamp-y, 0 ) ); } .os-dock-peek[ data-orientation = "right" ] { transform: translate( calc( -100% + var( --peek-clamp-x, 0px ) ), var( --peek-clamp-y, 0 ) ); } .os-dock-peek[ data-orientation = "bottom" ] { transform: translate( calc( -50% + var( --peek-clamp-x, 0px ) ), calc( -100% + var( --peek-clamp-y, 0px ) ) ); } /* ────────────────────────────────────────────────────────────────── Cards. ────────────────────────────────────────────────────────────────── */ .os-dock-peek__cards { display: flex; flex-direction: column; gap: 8px; min-width: 280px; /* Take up whatever vertical space the popover offers and scroll internally once the cards spill beyond it. Subtract the popover's padding so the scrollbar doesn't flush against the rounded outer edge. */ flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; /* Hide the scrollbar visually until hover — the peek surface is meant to feel weightless, not "yet another panel with a scrollbar." Falls back to the platform scrollbar where the custom-scrollbar pseudo isn't supported. */ scrollbar-width: thin; scrollbar-color: rgba( 255, 255, 255, 0.18 ) transparent; } /* Bottom dock: cards lay out as a horizontal reel and scroll on the X-axis instead of Y. */ .os-dock-peek[ data-orientation = "bottom" ] .os-dock-peek__cards { flex-direction: row; min-width: 0; overflow-x: auto; overflow-y: hidden; max-width: 100%; } .os-dock-peek[ data-orientation = "bottom" ] .os-dock-peek__card { /* Bottom-dock cards are narrower than left/right dock cards — a horizontal reel reads better with thumbnail-shaped tiles (Mission Control / Stage Manager) than with full-width sidebar-shaped ones. flex: 0 0 auto so a long reel doesn't squish each card down to nothing. */ flex: 0 0 auto; width: 200px; min-height: 116px; /* Cards translate from BELOW into the reel (towards the dock tile they peeled off of), so the directional motion still points back at the icon. */ transform: translateY( 10px ) scale( 0.96 ); } .os-dock-peek[ data-orientation = "bottom" ] .os-dock-peek__card--ghost { width: 200px; min-height: 116px; } .os-dock-peek[ data-orientation = "bottom" ].os-dock-peek--open .os-dock-peek__card { transform: translateY( 0 ) scale( 1 ); } .os-dock-peek__cards::-webkit-scrollbar { width: 8px; height: 8px; } .os-dock-peek__cards::-webkit-scrollbar-track { background: transparent; } .os-dock-peek__cards::-webkit-scrollbar-thumb { background: rgba( 255, 255, 255, 0.18 ); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; } .os-dock-peek__cards::-webkit-scrollbar-thumb:hover { background: rgba( 255, 255, 255, 0.32 ); background-clip: padding-box; } .os-dock-peek__card { /* Card frame — looks like a tiny window. */ display: flex; flex-direction: column; overflow: hidden; width: 280px; min-height: 92px; padding: 0; border: 1px solid rgba( 255, 255, 255, 0.10 ); border-radius: 10px; background: rgba( 255, 255, 255, 0.04 ); color: inherit; font: inherit; text-align: start; cursor: pointer; view-transition-name: var( --peek-card-vt-name, none ); /* Animation start state — cards begin slightly displaced toward the tile they "peeled off of". CSS resets to identity once the `--open` class on the parent triggers the transition. */ opacity: 0; transform: translateX( -10px ) scale( 0.96 ); transition: opacity 240ms ease-out var( --peek-card-delay, 0ms ), transform 320ms cubic-bezier( 0.34, 1.56, 0.64, 1 ) var( --peek-card-delay, 0ms ), border-color 180ms ease-out, box-shadow 180ms ease-out; } .os-dock-peek--open .os-dock-peek__card { opacity: 1; transform: translateX( 0 ) scale( 1 ); } /* Aero Peek hover: accent border + glow on every previewed card, not just minimumzd ones.*/ .os-dock-peek__card--instance:hover { border-color: var( --os-accent, #2271b1 ); box-shadow: 0 0 14px -2px var( --os-accent, #2271b1 ), 0 4px 14px -4px rgba( 0, 0, 0, 0.4 ); } .os-dock-peek__card:focus-visible { outline: 2px solid var( --os-ui-accent, #2271b1 ); outline-offset: 2px; } /* ────────────────────────────────────────────────────────────────── Mini-window chrome — instance card faux titlebar. ────────────────────────────────────────────────────────────────── */ .os-dock-peek__card-titlebar { display: flex; align-items: center; gap: 8px; padding: 8px 10px; /* Use the live window's titlebar background so the mini-window reads as a faithful shrink of the real window. Falls back to a neutral translucent gradient when the variable isn't set (early load, theme without the token, tests). */ background: var( --os-titlebar-bg-focused, linear-gradient( to bottom, rgba( 255, 255, 255, 0.10 ), rgba( 255, 255, 255, 0.04 ) ) ); color: var( --os-titlebar-color-focused, var( --os-fg, #fff ) ); border-bottom: 1px solid rgba( 0, 0, 0, 0.12 ); font-size: 12px; line-height: 1.2; } .os-dock-peek__card-dots { display: inline-flex; gap: 4px; flex: 0 0 auto; } .os-dock-peek__card-dots i { width: 8px; height: 8px; border-radius: 50%; background: rgba( 255, 255, 255, 0.22 ); } .os-dock-peek__card-dots i:nth-child( 1 ) { background: #ff5f57; } .os-dock-peek__card-dots i:nth-child( 2 ) { background: #febc2e; } .os-dock-peek__card-dots i:nth-child( 3 ) { background: #28c840; } .os-dock-peek__card-icon { flex: 0 0 auto; width: 14px; height: 14px; font-size: 14px; line-height: 1; opacity: 0.92; } .os-dock-peek__card-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; } /* ────────────────────────────────────────────────────────────────── Mini-window body — tinted by the page hue, ghosted content lines. The hue comes from `hashTitleToHue(window.id || title)`, so two instances of the same page (`edit.php` and `edit.php-2`) read as visually distinct without us having to invent unique colors. ────────────────────────────────────────────────────────────────── */ .os-dock-peek__card-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: 6px; padding: 12px 12px 14px; background: linear-gradient( 135deg, hsl( var( --peek-card-hue, 210 ) 55% 22% / 0.65 ), hsl( var( --peek-card-hue, 210 ) 55% 14% / 0.5 ) ); } .os-dock-peek__card-line { display: block; height: 6px; border-radius: 3px; background: rgba( 255, 255, 255, 0.16 ); } .os-dock-peek__card-line:nth-child( 1 ) { width: 80%; } .os-dock-peek__card-line:nth-child( 2 ) { width: 60%; } .os-dock-peek__card-line:nth-child( 3 ) { width: 70%; } /* Plugin-customized body — drop the default tinted background + ghost-line padding so the plugin's own markup gets a clean canvas. Plugins that want the tinted look back can re-add their own background; otherwise they get a transparent flex container. */ .os-dock-peek__card-body--custom { background: transparent; padding: 0; gap: 0; } /* ────────────────────────────────────────────────────────────────── Built-in renderer: OS Settings. A big tinted hero icon + a phrase + a row of three tab-icon chips representing the inner Settings tabs. Tinted with the user's WP profile accent so the card "belongs" to the active color scheme. ────────────────────────────────────────────────────────────────── */ .os-dock-peek__card-body--os-settings { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; padding: 14px 12px; background: linear-gradient( 135deg, color-mix( in srgb, var( --wp-admin-theme-color, #2271b1 ) 65%, #1d2327 ) 0%, color-mix( in srgb, var( --wp-admin-theme-color, #2271b1 ) 35%, #1d2327 ) 100% ); color: var( --os-ui-fg-on-accent, #fff ); } .os-dock-peek__os-hero { font-size: 32px; width: 32px; height: 32px; line-height: 1; opacity: 0.95; filter: drop-shadow( 0 1px 2px rgba( 0, 0, 0, 0.4 ) ); } .os-dock-peek__os-subtitle { font-size: 11px; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.85; text-shadow: 0 1px 2px rgba( 0, 0, 0, 0.3 ); } .os-dock-peek__os-tabs { display: flex; gap: 6px; margin-top: 2px; } .os-dock-peek__os-tab { width: 18px; height: 18px; font-size: 13px; line-height: 18px; border-radius: 4px; background: rgba( 255, 255, 255, 0.18 ); color: rgba( 255, 255, 255, 0.92 ); text-align: center; } /* ────────────────────────────────────────────────────────────────── Built-in renderer: Recycle Bin. Empty state: a single trash icon, dim. Full state: a stack of three "slips" peeking from behind the icon + a count chip in the corner. The `data-empty` attribute toggles between the two. ────────────────────────────────────────────────────────────────── */ .os-dock-peek__card-body--recycle-bin { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 14px 12px; background: linear-gradient( 135deg, #3a3f47 0%, #23272b 100% ); color: rgba( 255, 255, 255, 0.92 ); } .os-dock-peek__bin-stage { position: relative; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; } .os-dock-peek__bin-icon { font-size: 32px; width: 32px; height: 32px; line-height: 1; opacity: 0.95; filter: drop-shadow( 0 1px 2px rgba( 0, 0, 0, 0.4 ) ); z-index: 1; } .os-dock-peek__bin-stack { position: absolute; inset: 0; display: flex; align-items: flex-end; justify-content: center; pointer-events: none; opacity: 0; transition: opacity 200ms ease-out; } .os-dock-peek__card-body--recycle-bin[ data-empty = "false" ] .os-dock-peek__bin-stack { opacity: 1; } .os-dock-peek__bin-slip { position: absolute; width: 22px; height: 4px; border-radius: 1px; background: rgba( 255, 255, 255, 0.55 ); bottom: 6px; } .os-dock-peek__bin-slip:nth-child( 1 ) { transform: translate( -10px, -16px ) rotate( -8deg ); background: rgba( 255, 255, 255, 0.45 ); } .os-dock-peek__bin-slip:nth-child( 2 ) { transform: translate( 8px, -20px ) rotate( 6deg ); background: rgba( 255, 255, 255, 0.5 ); } .os-dock-peek__bin-slip:nth-child( 3 ) { transform: translate( -2px, -22px ) rotate( -2deg ); background: rgba( 255, 255, 255, 0.6 ); } .os-dock-peek__bin-label { font-size: 11px; font-weight: 500; letter-spacing: 0.02em; color: rgba( 255, 255, 255, 0.78 ); } .os-dock-peek__card-body--recycle-bin[ data-empty = "false" ] .os-dock-peek__bin-label { color: var( --wp-admin-theme-color, #4f9cff ); font-weight: 600; } /* ────────────────────────────────────────────────────────────────── Ghost Card — the "open new" affordance. Same geometry as instance cards (so the fan reads as one stack) but visually distinct via: * Dashed border (vs solid) * Lower base opacity * Breathing pulse (slow scale + opacity loop) * Centered "+" + label, no faux titlebar / body lines On hover, the dashes resolve to a solid line, the pulse stops, and opacity ramps to full — the card "solidifies" into the same visual weight as an instance card. ────────────────────────────────────────────────────────────────── */ .os-dock-peek__card--ghost { display: flex; align-items: center; justify-content: center; gap: 10px; width: 280px; min-height: 64px; padding: 16px; border-style: dashed; border-color: rgba( 255, 255, 255, 0.34 ); /* A self-contained dark translucent fill so the white label reads on light wallpapers / light color schemes too. The popover's parent surface uses `color-mix(--os-bg)`, which goes near-white on light themes — without our own backdrop the Ghost Card was white-on-white. */ background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.32 ) ); color: var( --os-ui-fg-on-accent, #fff ); text-shadow: 0 1px 2px rgba( 0, 0, 0, 0.45 ); font-size: 13px; font-weight: 500; animation: os-dock-peek-breathe 2400ms ease-in-out infinite; } .os-dock-peek__card--ghost:hover { border-style: solid; border-color: rgba( 255, 255, 255, 0.55 ); background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.45 ) ); color: var( --os-ui-fg-on-accent, #fff ); animation: none; transform: scale( 1.02 ); box-shadow: 0 4px 14px -4px rgba( 0, 0, 0, 0.5 ); } .os-dock-peek__card-plus { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; font-size: 22px; line-height: 1; font-weight: 300; /* Slightly more presence than before (was 0.42 alpha). The "+" is the only glyph the user sees in the resting state, so it has to read at a glance even against the breathing pulse's lower-opacity moments. */ color: rgba( 255, 255, 255, 0.78 ); text-shadow: 0 1px 2px rgba( 0, 0, 0, 0.45 ); transition: color 160ms ease-out; } .os-dock-peek__card--ghost:hover .os-dock-peek__card-plus { color: var( --os-ui-accent, #4f9cff ); } @keyframes os-dock-peek-breathe { 0%, 100% { opacity: 0.74; transform: scale( 1 ); } 50% { opacity: 1; transform: scale( 1.018 ); } } @media ( prefers-reduced-motion: reduce ) { .os-dock-peek, .os-dock-peek__card, .os-dock-peek__card--ghost { transition: opacity 80ms linear; animation: none !important; transform: none !important; } .os-dock-peek--open .os-dock-peek__card { transform: none; } } /* ────────────────────────────────────────────────────────────────── View Transitions — when supported, an instance card morphs into its window on click (and the Ghost Card morphs into the new spawned window). The transition group is named per-card via `--peek-card-vt-name`, set in JS at click time so the source + destination share an identity. ────────────────────────────────────────────────────────────────── */ @supports ( view-transition-name: none ) { ::view-transition-old( root ), ::view-transition-new( root ) { animation-duration: 280ms; animation-timing-function: cubic-bezier( 0.22, 1, 0.36, 1 ); } } /* Collapse minimized cards to just the titlebar. */ .os-dock-peek__card[data-state="minimized"] { min-height: 0; opacity: 0.85; } .os-dock-peek__card[data-state="minimized"] > :not(.os-dock-peek__card-titlebar) { display: none; }