| @@ -87,8 +87,19 @@ | ||
| 87 | 87 | color: var( --os-dock-icon-color, rgba( 255, 255, 255, 0.7 ) ); |
| 88 | 88 | transition: background-color 0.15s ease, transform 0.15s ease, color 0.15s ease; |
| 89 | 89 | } |
| 90 | 90 | |
| 91 | +/* | |
| 92 | + * The tile whose window is in front sits on a lit plate. Declared | |
| 93 | + * before the hover rule at the same specificity so the pointer still | |
| 94 | + * wins while it is over the tile. Falls back to nothing, which is | |
| 95 | + * what every placement drew before the token existed. | |
| 96 | + */ | |
| 97 | +.os-dock__item--focused .os-dock__item-primary { | |
| 98 | + background-color: var( --os-dock-item-bg-focused, transparent ); | |
| 99 | + color: var( --os-dock-icon-color-hover, var( --os-ui-fg-on-accent, #fff ) ); | |
| 100 | +} | |
| 101 | + | |
| 91 | 102 | .os-dock__item-primary:hover { |
| 92 | 103 | background-color: var( |
| 93 | 104 | --os-dock-item-bg-hover, |
| 94 | 105 | rgba( 255, 255, 255, 0.15 ) |
| @@ -113,21 +124,29 @@ | ||
| 113 | 124 | } |
| 114 | 125 | |
| 115 | 126 | /* Stacked card hint on hover — a second card peeks from behind the |
| 116 | 127 | icon when the tile has multiple open windows. Only visible on hover so |
| 117 | - it never conflicts with focused tiles. */ | |
| 118 | -.os-dock__item--stacked:hover .os-dock__item-primary::before, | |
| 119 | -.os-dock__item--stacked[data-peek-active] .os-dock__item-primary::before { | |
| 128 | + it never conflicts with focused tiles. | |
| 129 | + * | |
| 130 | + * Painted on the TILE, not on the button it hints at. The hover that | |
| 131 | + * arms this also scales the button, a transform makes a stacking | |
| 132 | + * context, and a `z-index: -1` child of one cannot escape it: the card | |
| 133 | + * landed above the button's own hover plate instead of behind the | |
| 134 | + * tile, reading as a second plate laid over the first. The tile is | |
| 135 | + * transformed by nothing, and a positioned child (the button) paints | |
| 136 | + * over a parent's `::before` at the same level, so the card sits where | |
| 137 | + * its name says without asking for a z-index at all. */ | |
| 138 | +.os-dock__item--stacked:hover::before, | |
| 139 | +.os-dock__item--stacked[ data-peek-active ]::before { | |
| 120 | 140 | content: ""; |
| 121 | 141 | position: absolute; |
| 122 | 142 | top: -4px; |
| 123 | 143 | inset-inline-start: -4px; |
| 124 | - width: 100%; | |
| 125 | - height: 100%; | |
| 126 | - border-radius: inherit; | |
| 144 | + width: 40px; | |
| 145 | + height: 40px; | |
| 146 | + border-radius: 10px; | |
| 127 | 147 | background: rgba( 255, 255, 255, 0.06 ); |
| 128 | 148 | border: 1px solid rgba( 255, 255, 255, 0.10 ); |
| 129 | - z-index: -1; | |
| 130 | 149 | pointer-events: none; |
| 131 | 150 | } |
| 132 | 151 | |
| 133 | 152 | /* Dashicon inside dock item. */ |
| @@ -249,9 +268,9 @@ | ||
| 249 | 268 | padding: 0; |
| 250 | 269 | border: 2px solid var( --os-dock-bg, rgba( 0, 0, 0, 0.4 ) ); |
| 251 | 270 | border-radius: 50%; |
| 252 | 271 | background: var( --wp-admin-theme-color, #2271b1 ); |
| 253 | - color: var( --os-ui-fg-on-accent, #fff ); | |
| 272 | + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) ); | |
| 254 | 273 | cursor: pointer; |
| 255 | 274 | transition: background-color 0.15s ease, transform 0.15s ease; |
| 256 | 275 | z-index: 1; |
| 257 | 276 | } |
| @@ -1003,17 +1022,26 @@ | ||
| 1003 | 1022 | * general problem. The inline padding is symmetric so the tile cluster |
| 1004 | 1023 | * stays centred in the pill. `min-width: 0` lets the wrapper shrink below its |
| 1005 | 1024 | * content's natural width, so the dock pill's `max-width` is what |
| 1006 | 1025 | * decides when scroll kicks in instead of the content forcing the |
| 1007 | - * pill wider. `justify-content: center` keeps tiles balanced inside | |
| 1008 | - * the pill when content fits; when it overflows, scroll engages and | |
| 1009 | - * items extend past the visible area. | |
| 1026 | + * pill wider. `justify-content: safe center` keeps tiles balanced inside | |
| 1027 | + * the pill when content fits; when it overflows, the `safe` keyword | |
| 1028 | + * ensures alignment falls back to `start` so `scrollLeft = 0` reveals the | |
| 1029 | + * first item and all items remain scrollable without clipping. Bare | |
| 1030 | + * `center` put the leading tiles at a negative offset, and a scroll | |
| 1031 | + * container clamps `scrollLeft` at 0, so Dashboard and its neighbours | |
| 1032 | + * were clipped with no way to scroll back to them. **The plain `start` | |
| 1033 | + * above it is the fallback, not a leftover** — a browser that cannot | |
| 1034 | + * parse the overflow-alignment keyword drops the `safe center` | |
| 1035 | + * declaration and keeps that one, which left-aligns the tiles rather | |
| 1036 | + * than hiding them. Deleting it restores the bug on those browsers. | |
| 1010 | 1037 | */ |
| 1011 | 1038 | .os-dock[ data-os-dock-placement="bottom" ] .os-dock__scroll { |
| 1012 | 1039 | display: flex; |
| 1013 | 1040 | flex-direction: row; |
| 1014 | 1041 | align-items: center; |
| 1015 | - justify-content: center; | |
| 1042 | + justify-content: start; | |
| 1043 | + justify-content: safe center; | |
| 1016 | 1044 | gap: 6px; |
| 1017 | 1045 | flex: 1 1 auto; |
| 1018 | 1046 | min-width: 0; |
| 1019 | 1047 | padding-top: 4px; |
| @@ -1243,6 +1271,186 @@ | ||
| 1243 | 1271 | .os-dock[ data-os-dock-placement="right" ] |
| 1244 | 1272 | .os-dock__item[ data-system-id="os-exit" ] |
| 1245 | 1273 | .os-dock__item-primary:hover { |
| 1246 | 1274 | transform: none; |
| 1275 | + } | |
| 1276 | +} | |
| 1277 | + | |
| 1278 | +/* ------------------------------------------------------------------ | |
| 1279 | + * Dynamic dock behavior — OpenStation Preferences → Appearance → | |
| 1280 | + * Desktop layout, persisted as `dockBehavior` (the dock) and | |
| 1281 | + * `sideDockBehavior` (the Split sidebar) and worn by each rail as | |
| 1282 | + * `data-os-dock-behavior` — an attribute PER RAIL rather than a body | |
| 1283 | + * class, because Split's two rails answer independently. PHP stamps | |
| 1284 | + * the dock on first paint; the apply pass and `src/dock-behavior.ts` | |
| 1285 | + * re-stamp on every change and rebuild. `static` is the absence of | |
| 1286 | + * rules. | |
| 1287 | + * | |
| 1288 | + * A dynamic rail that is not `os-dock--revealed` is PARKED: it | |
| 1289 | + * collapses into a thin indicator line hugging its edge — the iOS | |
| 1290 | + * home indicator, one line that says "there is a dock here" — and | |
| 1291 | + * expands back into the full rail when the pointer comes for it. | |
| 1292 | + * `src/dock-behavior.ts` owns the flip (a full-width edge zone, the | |
| 1293 | + * rail's own box, its flyouts, keyboard focus) and runs it through | |
| 1294 | + * the View Transitions API, so the line morphs into the pill and | |
| 1295 | + * back; the rules below only describe the two resting states and | |
| 1296 | + * tune the morph. There is deliberately no `:hover` here — a state | |
| 1297 | + * CSS flipped on its own would jump instead of morphing. | |
| 1298 | + * | |
| 1299 | + * The line is the rail itself, not a separate element: same node, | |
| 1300 | + * same `view-transition-name`, which is what lets the browser animate | |
| 1301 | + * one box into the other. Its children are hidden while parked so | |
| 1302 | + * the line is a line. A side rail leaves the flex row for the | |
| 1303 | + * duration, so the area grows to the full width and the rail rides | |
| 1304 | + * over it when summoned; the work area reserves nothing for a | |
| 1305 | + * dynamic rail (`src/work-area/index.ts`). | |
| 1306 | + * ------------------------------------------------------------------ */ | |
| 1307 | + | |
| 1308 | +/* | |
| 1309 | + * Side rails overlay the area in both states — the flex row must not | |
| 1310 | + * shrink for a rail that is only sometimes there. | |
| 1311 | + */ | |
| 1312 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ], | |
| 1313 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ] { | |
| 1314 | + position: absolute; | |
| 1315 | + inset-block: 0; | |
| 1316 | +} | |
| 1317 | + | |
| 1318 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ] { | |
| 1319 | + inset-inline-start: 0; | |
| 1320 | +} | |
| 1321 | + | |
| 1322 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ] { | |
| 1323 | + inset-inline-end: 0; | |
| 1324 | +} | |
| 1325 | + | |
| 1326 | +/* | |
| 1327 | + * Parked — the indicator line. Everything that makes the pill a | |
| 1328 | + * pill (padding, border, blur, texture, tile gap) comes off, and the | |
| 1329 | + * box is the line's size. `overflow: hidden` clips whatever a tile | |
| 1330 | + * paints past its box during the morph. | |
| 1331 | + */ | |
| 1332 | +.os-dock[ data-os-dock-behavior="dynamic" ]:not( .os-dock--revealed ) { | |
| 1333 | + min-width: 0; | |
| 1334 | + min-height: 0; | |
| 1335 | + max-width: none; | |
| 1336 | + padding: 0; | |
| 1337 | + gap: 0; | |
| 1338 | + border: 0; | |
| 1339 | + border-radius: 999px; | |
| 1340 | + overflow: hidden; | |
| 1341 | + background-color: var( | |
| 1342 | + --os-dock-indicator-bg, | |
| 1343 | + var( --os-dock-item-outline, rgba( 255, 255, 255, 0.55 ) ) | |
| 1344 | + ); | |
| 1345 | + background-image: none; | |
| 1346 | + backdrop-filter: none; | |
| 1347 | + -webkit-backdrop-filter: none; | |
| 1348 | + box-shadow: none; | |
| 1349 | + cursor: pointer; | |
| 1350 | +} | |
| 1351 | + | |
| 1352 | +.os-dock[ data-os-dock-behavior="dynamic" ]:not( .os-dock--revealed ) > * { | |
| 1353 | + display: none; | |
| 1354 | +} | |
| 1355 | + | |
| 1356 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="bottom" ]:not( .os-dock--revealed ) { | |
| 1357 | + width: var( --os-dock-indicator-length, 180px ); | |
| 1358 | + height: var( --os-dock-indicator-thickness, 5px ); | |
| 1359 | + bottom: 8px; | |
| 1360 | +} | |
| 1361 | + | |
| 1362 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ]:not( .os-dock--revealed ), | |
| 1363 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ]:not( .os-dock--revealed ) { | |
| 1364 | + width: var( --os-dock-indicator-thickness, 5px ); | |
| 1365 | + height: var( --os-dock-indicator-length, 180px ); | |
| 1366 | + inset-block: calc( 50% - var( --os-dock-indicator-length, 180px ) / 2 ); | |
| 1367 | +} | |
| 1368 | + | |
| 1369 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ]:not( .os-dock--revealed ) { | |
| 1370 | + inset-inline-start: 8px; | |
| 1371 | +} | |
| 1372 | + | |
| 1373 | +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ]:not( .os-dock--revealed ) { | |
| 1374 | + inset-inline-end: 8px; | |
| 1375 | +} | |
| 1376 | + | |
| 1377 | +/* | |
| 1378 | + * The morph. With the API, `src/dock-behavior.ts` names the rail for | |
| 1379 | + * the duration of one flip and the browser animates the old box into | |
| 1380 | + * the new one; the rail's contents cross-fade underneath. The root is | |
| 1381 | + * opted out of the snapshot so the rest of the desktop keeps | |
| 1382 | + * running while the line becomes a pill — a frozen page for 260ms | |
| 1383 | + * on every reveal would be the animation costing more than it | |
| 1384 | + * gives. Without the API, the properties that can transition do. | |
| 1385 | + */ | |
| 1386 | +@supports ( view-transition-name: none ) { | |
| 1387 | + html.os-dock-vt { | |
| 1388 | + view-transition-name: none; | |
| 1389 | + } | |
| 1390 | + | |
| 1391 | + html.os-dock-vt::view-transition-old( root ), | |
| 1392 | + html.os-dock-vt::view-transition-new( root ) { | |
| 1393 | + animation: none; | |
| 1394 | + } | |
| 1395 | + | |
| 1396 | + /* | |
| 1397 | + * The transition layer must not catch the pointer. By default it | |
| 1398 | + * does — the whole page reads as inert until the morph settles — | |
| 1399 | + * and with the root opted out of the snapshot that is worse than | |
| 1400 | + * pointless: the live desktop is right there under a layer that | |
| 1401 | + * swallows every mouseup and every pointerenter for 260ms. The | |
| 1402 | + * behavior module already holds a park back while a button is | |
| 1403 | + * down; this is the other half, so a hover that lands on a tile | |
| 1404 | + * while the rail is still morphing out starts its hover-intent | |
| 1405 | + * timer instead of waiting for the next pointer movement. | |
| 1406 | + */ | |
| 1407 | + html.os-dock-vt::view-transition { | |
| 1408 | + pointer-events: none; | |
| 1409 | + } | |
| 1410 | + | |
| 1411 | + ::view-transition-group( os-dock-os-dock ), | |
| 1412 | + ::view-transition-group( os-dock-os-side-dock ) { | |
| 1413 | + animation-duration: 260ms; | |
| 1414 | + animation-timing-function: cubic-bezier( 0.22, 1, 0.36, 1 ); | |
| 1415 | + } | |
| 1416 | + | |
| 1417 | + ::view-transition-old( os-dock-os-dock ), | |
| 1418 | + ::view-transition-old( os-dock-os-side-dock ) { | |
| 1419 | + animation: os-dock-vt-out 120ms ease-out both; | |
| 1420 | + } | |
| 1421 | + | |
| 1422 | + ::view-transition-new( os-dock-os-dock ), | |
| 1423 | + ::view-transition-new( os-dock-os-side-dock ) { | |
| 1424 | + animation: os-dock-vt-in 180ms ease-out 80ms both; | |
| 1425 | + } | |
| 1426 | + | |
| 1427 | + @keyframes os-dock-vt-out { | |
| 1428 | + to { | |
| 1429 | + opacity: 0; | |
| 1430 | + } | |
| 1431 | + } | |
| 1432 | + | |
| 1433 | + @keyframes os-dock-vt-in { | |
| 1434 | + from { | |
| 1435 | + opacity: 0; | |
| 1436 | + } | |
| 1437 | + } | |
| 1438 | +} | |
| 1439 | + | |
| 1440 | +@supports not ( view-transition-name: none ) { | |
| 1441 | + .os-dock[ data-os-dock-behavior="dynamic" ] { | |
| 1442 | + transition: | |
| 1443 | + width 240ms ease, | |
| 1444 | + height 240ms ease, | |
| 1445 | + opacity 240ms ease, | |
| 1446 | + border-radius 240ms ease, | |
| 1447 | + bottom 240ms ease, | |
| 1448 | + inset-block 240ms ease; | |
| 1449 | + } | |
| 1450 | +} | |
| 1451 | + | |
| 1452 | +@media ( prefers-reduced-motion: reduce ) { | |
| 1453 | + .os-dock[ data-os-dock-behavior="dynamic" ] { | |
| 1454 | + transition: none; | |
| 1247 | 1455 | } |
| 1248 | 1456 | } |