| @@ -363,21 +363,32 @@ | ||
| 363 | 363 | } |
| 364 | 364 | } |
| 365 | 365 | |
| 366 | 366 | /* |
| 367 | - * Minimized state — fade + scale down. | |
| 368 | - * Uses opacity + pointer-events instead of display: none so the | |
| 369 | - * CSS transition can animate the change. The window is invisible | |
| 370 | - * and non-interactive but stays in the DOM for a smooth restore. | |
| 367 | + * Minimized state — window is hidden and non-interactive at the dock. | |
| 368 | + * Uses opacity + pointer-events + transition: none so neither state | |
| 369 | + * changes nor WAAPI animation teardown trigger accidental transitions. | |
| 371 | 370 | */ |
| 372 | 371 | .os-window--minimized { |
| 373 | 372 | opacity: 0; |
| 374 | - transform: scale(0.92) translateY(16px); | |
| 375 | 373 | pointer-events: none; |
| 376 | - will-change: transform, opacity; | |
| 374 | + transition: none !important; | |
| 377 | 375 | } |
| 378 | 376 | |
| 379 | 377 | /* |
| 378 | + * Genie helpers — active only during the WAAPI flight to/from the | |
| 379 | + * dock. Disable the base `left/top/width/height/transform/opacity` | |
| 380 | + * transition so the CSS transition doesn't fight the WAAPI | |
| 381 | + * `element.animate()` that drives the scale-to-dock motion. The | |
| 382 | + * minimized class is applied AFTER the minimizing flight lands, and | |
| 383 | + * the restoring class is removed AFTER the restoring flight lands. | |
| 384 | + */ | |
| 385 | +.os-window--minimizing, | |
| 386 | +.os-window--restoring { | |
| 387 | + transition: none !important; | |
| 388 | +} | |
| 389 | + | |
| 390 | +/* | |
| 380 | 391 | * `pointer-events: none` on the window root above is not enough on |
| 381 | 392 | * its own: a number of descendants (most notably `.os- |
| 382 | 393 | * file-tile`, but also other tile renderers) explicitly set |
| 383 | 394 | * `pointer-events: auto` so the wallpaper / folder layer doesn't |
| @@ -417,8 +428,13 @@ | ||
| 417 | 428 | } |
| 418 | 429 | .os-window--opening { |
| 419 | 430 | animation-duration: 0.01ms !important; |
| 420 | 431 | } |
| 432 | + .os-window--minimizing, | |
| 433 | + .os-window--restoring { | |
| 434 | + animation: none !important; | |
| 435 | + transition: none !important; | |
| 436 | + } | |
| 421 | 437 | } |
| 422 | 438 | |
| 423 | 439 | /* |
| 424 | 440 | * RTL note: resize handles use physical `left` / `right` anchors |
| @@ -462,8 +478,152 @@ | ||
| 462 | 478 | } |
| 463 | 479 | |
| 464 | 480 | .os-snap-preview--visible { |
| 465 | 481 | opacity: 1; |
| 482 | +} | |
| 483 | + | |
| 484 | +/* --------------------------------------------------------------- | |
| 485 | + * Grid snap — the 6×6 desk shown while Option / Alt is held during a | |
| 486 | + * drag (see window-manager/grid-snap.ts). | |
| 487 | + * | |
| 488 | + * The overlay is the work area; the lines are one repeating gradient | |
| 489 | + * sized to a cell, and a cell is a fraction of the overlay — so the | |
| 490 | + * same rule draws the grid at any dimensions, on any display, and | |
| 491 | + * follows the work area if the dock folds away mid-drag. The target | |
| 492 | + * is the span the window will land on, in the same accent as the | |
| 493 | + * edge-snap preview so both previews read as one vocabulary. | |
| 494 | + * --------------------------------------------------------------- */ | |
| 495 | +.os-grid-snap { | |
| 496 | + position: absolute; | |
| 497 | + /* Same layer as the edge-snap preview: under the windows, so the | |
| 498 | + one being dragged stays on top of its own landing zone. */ | |
| 499 | + z-index: 99; | |
| 500 | + pointer-events: none; | |
| 501 | + opacity: 0; | |
| 502 | + transition: opacity 0.2s ease; | |
| 503 | + --os-grid-snap-cols: 6; | |
| 504 | + --os-grid-snap-rows: 6; | |
| 505 | + --os-grid-snap-line: color-mix( | |
| 506 | + in srgb, | |
| 507 | + var( --wp-admin-theme-color, #2271b1 ) 55%, | |
| 508 | + transparent | |
| 509 | + ); | |
| 510 | + background-image: | |
| 511 | + linear-gradient( to right, var( --os-grid-snap-line ) 1px, transparent 1px ), | |
| 512 | + linear-gradient( to bottom, var( --os-grid-snap-line ) 1px, transparent 1px ); | |
| 513 | + background-size: | |
| 514 | + calc( 100% / var( --os-grid-snap-cols ) ) 100%, | |
| 515 | + 100% calc( 100% / var( --os-grid-snap-rows ) ); | |
| 516 | + /* The gradient draws a line at the START of every cell; the | |
| 517 | + inset ring supplies the far edges so the grid is closed. */ | |
| 518 | + box-shadow: inset 0 0 0 1px var( --os-grid-snap-line ); | |
| 519 | +} | |
| 520 | + | |
| 521 | +.os-grid-snap--visible { | |
| 522 | + opacity: 1; | |
| 523 | +} | |
| 524 | + | |
| 525 | +.os-grid-snap__target { | |
| 526 | + position: absolute; | |
| 527 | + background: color-mix( | |
| 528 | + in srgb, | |
| 529 | + var( --wp-admin-theme-color, #2271b1 ) 16%, | |
| 530 | + transparent | |
| 531 | + ); | |
| 532 | + border: 2px solid var( --wp-admin-theme-color, #2271b1 ); | |
| 533 | + border-radius: 10px; | |
| 534 | + box-shadow: | |
| 535 | + 0 0 0 1px rgba( 255, 255, 255, 0.15 ) inset, | |
| 536 | + 0 12px 36px rgba( 0, 0, 0, 0.22 ); | |
| 537 | + /* Geometry animates so a cell-to-cell change slides rather than | |
| 538 | + jumps; short, so it never lags the pointer by a cell. */ | |
| 539 | + transition: | |
| 540 | + left 0.12s cubic-bezier( 0.2, 0, 0.2, 1 ), | |
| 541 | + top 0.12s cubic-bezier( 0.2, 0, 0.2, 1 ), | |
| 542 | + width 0.12s cubic-bezier( 0.2, 0, 0.2, 1 ), | |
| 543 | + height 0.12s cubic-bezier( 0.2, 0, 0.2, 1 ); | |
| 544 | +} | |
| 545 | + | |
| 546 | +/* | |
| 547 | + * The span, as "cols × rows", in the MIDDLE of the target. A user | |
| 548 | + * picking a 3×2 wants to know it is 3×2 before letting go; the count | |
| 549 | + * is what makes the grid a tool rather than a decoration, but only | |
| 550 | + * if it is read, and in the corner it was not. The pointer is | |
| 551 | + * somewhere inside the target by definition, so the centre is the | |
| 552 | + * shortest average trip for the eye; on a 2×5 the corner could be | |
| 553 | + * several hundred pixels from the hand. | |
| 554 | + * | |
| 555 | + * Centred rather than pinned to the pointer on purpose: a badge that | |
| 556 | + * tracked the cursor would rewrite its position every move, and a | |
| 557 | + * number that slides while you read it is harder to read than one | |
| 558 | + * that waits. It only moves when the span does. | |
| 559 | + */ | |
| 560 | +.os-grid-snap__target::after { | |
| 561 | + content: attr( data-cols ) ' × ' attr( data-rows ); | |
| 562 | + position: absolute; | |
| 563 | + top: 50%; | |
| 564 | + left: 50%; | |
| 565 | + transform: translate( -50%, -50% ); | |
| 566 | + padding: 2px 7px; | |
| 567 | + border-radius: 6px; | |
| 568 | + background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) ); | |
| 569 | + color: var( --os-ui-fg-on-accent, #fff ); | |
| 570 | + font-size: 11px; | |
| 571 | + font-weight: 600; | |
| 572 | + letter-spacing: 0.03em; | |
| 573 | + font-variant-numeric: tabular-nums; | |
| 574 | +} | |
| 575 | + | |
| 576 | +/* | |
| 577 | + * The desk in grid mode. Every OTHER window recedes so the grid and | |
| 578 | + * the landing zone read through them; the one in hand stays solid — | |
| 579 | + * it is the thing being placed, and the user needs to see it, not | |
| 580 | + * through it. Translucent enough that the grid shows, opaque enough | |
| 581 | + * that the other windows are still recognisably where they are, | |
| 582 | + * because "will this land on top of Posts?" is the question the grid | |
| 583 | + * is there to answer. | |
| 584 | + * | |
| 585 | + * Opacity only: a blur or a desaturate on a live iframe is a | |
| 586 | + * per-frame filter on a large surface, and the drag has to stay | |
| 587 | + * pixel-accurate at 120Hz. Minimized windows are already hidden and | |
| 588 | + * need no rule. | |
| 589 | + */ | |
| 590 | +.os-area--grid-snapping .os-window:not( .os-window--grid-snapping ) { | |
| 591 | + opacity: 0.45; | |
| 592 | + transition: opacity 0.16s ease; | |
| 593 | +} | |
| 594 | + | |
| 595 | +/* A shake moved the anchor: one pulse so the reset is seen, not inferred. */ | |
| 596 | +.os-grid-snap__target--reset { | |
| 597 | + animation: os-grid-snap-reset 0.32s ease; | |
| 598 | +} | |
| 599 | + | |
| 600 | +@keyframes os-grid-snap-reset { | |
| 601 | + 0% { | |
| 602 | + box-shadow: | |
| 603 | + 0 0 0 1px rgba( 255, 255, 255, 0.15 ) inset, | |
| 604 | + 0 0 0 0 var( --wp-admin-theme-color, #2271b1 ); | |
| 605 | + } | |
| 606 | + 60% { | |
| 607 | + box-shadow: | |
| 608 | + 0 0 0 1px rgba( 255, 255, 255, 0.15 ) inset, | |
| 609 | + 0 0 0 10px transparent; | |
| 610 | + } | |
| 611 | + 100% { | |
| 612 | + box-shadow: | |
| 613 | + 0 0 0 1px rgba( 255, 255, 255, 0.15 ) inset, | |
| 614 | + 0 12px 36px rgba( 0, 0, 0, 0.22 ); | |
| 615 | + } | |
| 616 | +} | |
| 617 | + | |
| 618 | +@media ( prefers-reduced-motion: reduce ) { | |
| 619 | + .os-grid-snap, | |
| 620 | + .os-grid-snap__target { | |
| 621 | + transition: none; | |
| 622 | + } | |
| 623 | + .os-grid-snap__target--reset { | |
| 624 | + animation: none; | |
| 625 | + } | |
| 466 | 626 | } |
| 467 | 627 | |
| 468 | 628 | /* |
| 469 | 629 | * Snapped window state — `--snapped-left` / `--snapped-right` carry |