PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 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 All 37 releases
← All changes | assets/css/dock.css +503 -114 1.0.0 → 1.1.13 View file →
@@ -50,9 +50,9 @@
50 50 position: relative;
51 51 display: flex;
52 52 flex-direction: column;
53 53 align-items: center;
54 - width: 40px;
54 + width: var( --os-dock-item-size, 40px );
55 55 flex-shrink: 0;
56 56 }
57 57
58 58 /* Primary icon button — opens or focuses the page. */
@@ -60,12 +60,12 @@
60 60 position: relative;
61 61 display: flex;
62 62 align-items: center;
63 63 justify-content: center;
64 - width: 40px;
65 - height: 40px;
64 + width: var( --os-dock-item-size, 40px );
65 + height: var( --os-dock-item-size, 40px );
66 66 border: none;
67 - border-radius: 10px;
67 + border-radius: var( --os-dock-item-radius, 10px );
68 68 cursor: pointer;
69 69 padding: 0;
70 70 background-color: transparent;
71 71 /*
@@ -87,8 +87,32 @@
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 +
102 +/*
103 + * DOCK_ITEM_FOCUSED texture slot — the face of the tile whose window is
104 + * in front, and of any tile held down: a key that stays pressed while
105 + * its window is up. Falls back to the resting face.
106 + */
107 +.os-dock__item--focused .os-dock__item-primary,
108 +.os-dock__item-primary:active {
109 + background-image: var(
110 + --os-dock-item-image-focused,
111 + var( --os-dock-item-image, none )
112 + );
113 +}
114 +
91 115 .os-dock__item-primary:hover {
92 116 background-color: var(
93 117 --os-dock-item-bg-hover,
94 118 rgba( 255, 255, 255, 0.15 )
@@ -96,13 +120,13 @@
96 120 color: var(
97 121 --os-dock-icon-color-hover,
98 122 var( --os-ui-fg-on-accent, #fff )
99 123 );
100 - transform: scale( 1.1 );
124 + transform: var( --os-dock-item-hover-transform, scale( 1.1 ) );
101 125 }
102 126
103 127 .os-dock__item-primary:active {
104 - transform: scale( 0.95 );
128 + transform: var( --os-dock-item-press-transform, scale( 0.95 ) );
105 129 }
106 130
107 131 .os-dock__item-primary:focus-visible {
108 132 outline: 2px solid var(
@@ -113,21 +137,29 @@
113 137 }
114 138
115 139 /* Stacked card hint on hover — a second card peeks from behind the
116 140 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 {
141 + it never conflicts with focused tiles.
142 + *
143 + * Painted on the TILE, not on the button it hints at. The hover that
144 + * arms this also scales the button, a transform makes a stacking
145 + * context, and a `z-index: -1` child of one cannot escape it: the card
146 + * landed above the button's own hover plate instead of behind the
147 + * tile, reading as a second plate laid over the first. The tile is
148 + * transformed by nothing, and a positioned child (the button) paints
149 + * over a parent's `::before` at the same level, so the card sits where
150 + * its name says without asking for a z-index at all. */
151 +.os-dock__item--stacked:hover::before,
152 +.os-dock__item--stacked[ data-peek-active ]::before {
120 153 content: "";
121 154 position: absolute;
122 155 top: -4px;
123 156 inset-inline-start: -4px;
124 - width: 100%;
125 - height: 100%;
126 - border-radius: inherit;
157 + width: 40px;
158 + height: 40px;
159 + border-radius: 10px;
127 160 background: rgba( 255, 255, 255, 0.06 );
128 161 border: 1px solid rgba( 255, 255, 255, 0.10 );
129 - z-index: -1;
130 162 pointer-events: none;
131 163 }
132 164
133 165 /* Dashicon inside dock item. */
@@ -249,9 +281,9 @@
249 281 padding: 0;
250 282 border: 2px solid var( --os-dock-bg, rgba( 0, 0, 0, 0.4 ) );
251 283 border-radius: 50%;
252 284 background: var( --wp-admin-theme-color, #2271b1 );
253 - color: var( --os-ui-fg-on-accent, #fff );
285 + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) );
254 286 cursor: pointer;
255 287 transition: background-color 0.15s ease, transform 0.15s ease;
256 288 z-index: 1;
257 289 }
@@ -510,9 +542,9 @@
510 542
511 543 /* Shared tooltip — placement-specific anchor modifiers applied by JS. */
512 544 .os-dock__tooltip {
513 545 position: fixed;
514 - padding: 6px 12px;
546 + padding: var( --os-tooltip-padding, 6px 12px );
515 547 background: var(
516 548 --os-tooltip-bg,
517 549 var( --os-ui-scrim, rgba( 0, 0, 0, 0.85 ) )
518 550 );
@@ -519,13 +551,15 @@
519 551 color: var(
520 552 --os-tooltip-fg,
521 553 var( --os-ui-fg-on-accent, #fff )
522 554 );
523 - font-size: 12px;
555 + font-size: var( --os-tooltip-font-size, 12px );
524 556 font-weight: 400;
525 557 line-height: 1.4;
526 558 white-space: nowrap;
527 - border-radius: 6px;
559 + border: var( --os-tooltip-border, 0 );
560 + border-radius: var( --os-tooltip-radius, 6px );
561 + box-shadow: var( --os-tooltip-shadow, none );
528 562 pointer-events: none;
529 563 z-index: calc( var( --os-z-dock ) + 1 );
530 564 opacity: 0;
531 565 transition: opacity 0.15s ease, transform 0.15s ease;
@@ -632,9 +666,9 @@
632 666 transform: translateY( -50% );
633 667 width: 4px;
634 668 height: 4px;
635 669 border-radius: 50%;
636 - background: var( --os-dock-item-outline, #fff );
670 + background: var( --os-dock-active-indicator, var( --os-dock-item-outline, #fff ) );
637 671 }
638 672
639 673 .os-dock[ data-os-dock-placement="left" ]
640 674 .os-dock__item--focused::before {
@@ -658,9 +692,9 @@
658 692 width: 6px;
659 693 height: 6px;
660 694 border-radius: 50%;
661 695 background: transparent;
662 - border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
696 + border: 1px solid var( --os-dock-active-indicator, var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) ) );
663 697 }
664 698
665 699 /* Right dock: indicator on the opposite edge (inside-facing). */
666 700 .os-dock[ data-os-dock-placement="right" ]
@@ -672,9 +706,9 @@
672 706 transform: translateY( -50% );
673 707 width: 4px;
674 708 height: 4px;
675 709 border-radius: 50%;
676 - background: var( --os-dock-item-outline, #fff );
710 + background: var( --os-dock-active-indicator, var( --os-dock-item-outline, #fff ) );
677 711 }
678 712
679 713 .os-dock[ data-os-dock-placement="right" ]
680 714 .os-dock__item--focused::before {
@@ -688,9 +722,9 @@
688 722 width: 6px;
689 723 height: 6px;
690 724 border-radius: 50%;
691 725 background: transparent;
692 - border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
726 + border: 1px solid var( --os-dock-active-indicator, var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) ) );
693 727 }
694 728
695 729 /* "Open another" chip — left: right of tile; right: left of tile. */
696 730 .os-dock[ data-os-dock-placement="left" ]
@@ -716,34 +750,51 @@
716 750 .os-dock__item-new:hover {
717 751 transform: translateY( -50% ) scale( 1.1 );
718 752 }
719 753
720 -/* Separator — horizontal hairline between menu tiles and system
721 - * tiles. With the `__scroll` / `__pinned` split the system separator
722 - * lives at the top of `__pinned`; flex-flow handles the "system
723 - * tiles at the bottom" placement, no margin-top: auto needed. The
724 - * `--group` variant is used inline between core and plugin menu
725 - * tiles in `__scroll`; it keeps the hairline styling and sits
726 - * exactly where it's inserted. */
754 +/* Separator — the line between two groups of tiles. With the
755 + * `__scroll` / `__pinned` split the system separator lives at the top
756 + * of `__pinned`; flex-flow handles the "system tiles at the bottom"
757 + * placement, no margin-top: auto needed. The `--group` variant is used
758 + * inline between core and plugin menu tiles in `__scroll` and sits
759 + * exactly where it's inserted.
760 + *
761 + * ONE treatment for every division in the rail. The selector carries
762 + * only the base class on purpose: `--group` elements carry it too, so
763 + * both boundaries resolve here and cannot drift apart. Two weights in
764 + * one short rail read as two unrelated ideas rather than one system,
765 + * which is why the earlier loud-plus-hairline pairing is gone.
766 + *
767 + * A gradient, not a flat fill: the line is brightest where the eye
768 + * lands and gone by the time it reaches the rail's padding, so it
769 + * never terminates in a visible stub against the glass. The soft glow
770 + * around it is what keeps a 2px line from disappearing into the dock
771 + * tint; nothing is drawn on the line itself. */
727 772 .os-dock[ data-os-dock-placement="left" ]
728 773 .os-dock__separator,
729 774 .os-dock[ data-os-dock-placement="right" ]
730 775 .os-dock__separator {
776 + position: relative;
731 777 width: 60%;
732 - height: 1px;
733 - margin: 8px auto 4px;
734 - background: var( --os-dock-border );
778 + height: 2px;
779 + margin: 12px auto;
780 + border-radius: 2px;
781 + background: linear-gradient(
782 + to right,
783 + transparent 0%,
784 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 28%,
785 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 72%,
786 + transparent 100%
787 + );
788 + box-shadow: 0 0 10px
789 + color-mix(
790 + in srgb,
791 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 45%,
792 + transparent
793 + );
735 794 flex-shrink: 0;
736 795 }
737 796
738 -.os-dock[ data-os-dock-placement="left" ]
739 - .os-dock__separator--group,
740 -.os-dock[ data-os-dock-placement="right" ]
741 - .os-dock__separator--group {
742 - margin: 10px auto;
743 - background: rgba( 255, 255, 255, 0.22 );
744 -}
745 -
746 797 /*
747 798 * Hide the system separator when nothing precedes it — on a clean
748 799 * install (or a low-cap user with zero menu items) the separator
749 800 * would render as a stray hairline with nothing to divide.
@@ -763,8 +814,9 @@
763 814 .os-dock__separator {
764 815 display: none;
765 816 }
766 817
818 +
767 819 /*
768 820 * Drop an empty `__scroll` from the flex flow entirely. When only
769 821 * system tiles are present (e.g. just the Recycle Bin), the empty
770 822 * wrapper would otherwise still occupy the rail: its `flex-grow`
@@ -823,16 +875,22 @@
823 875 * viewport floor. Absolutely positioned so it floats as pure chrome
824 876 * rather than stealing vertical space from the shell body.
825 877 * ------------------------------------------------------------------ */
826 878
879 +/*
880 + * The pill's geometry is tokenized so a theme can draw a different
881 + * object along the same edge: a full-width bar flush with the floor
882 + * is `width: 100%`, `bottom: 0`, radius 0. The work area measures the
883 + * rail's real box, so windows keep clear of whatever shape it takes.
884 + */
827 885 .os-dock[ data-os-dock-placement="bottom" ] {
828 886 position: absolute;
829 887 inset-inline-start: 0;
830 888 inset-inline-end: 0;
831 - bottom: 12px;
889 + bottom: var( --os-dock-floating-offset, 12px );
832 890 margin: 0 auto;
833 - width: fit-content;
834 - max-width: calc( 100% - 32px );
891 + width: var( --os-dock-floating-width, fit-content );
892 + max-width: var( --os-dock-floating-max-width, calc( 100% - 32px ) );
835 893 flex-direction: row;
836 894 justify-content: center;
837 895 /* Top padding is 4px instead of 8px so that the inner `__scroll`
838 896 * wrapper can claim its own 4px padding-top for badge clearance
@@ -838,11 +896,11 @@
838 896 * wrapper can claim its own 4px padding-top for badge clearance
839 897 * (the tile's badge sits at top: -3px). Net vertical chrome stays
840 898 * symmetric: 4px pill padding + 4px wrapper padding above the
841 899 * tile, 8px pill padding below — tile is 8px from both edges. */
842 - padding: 4px 12px 8px;
843 - gap: 6px;
844 - border-radius: 18px;
900 + padding: var( --os-dock-floating-padding, 4px 12px 8px );
901 + gap: var( --os-dock-floating-gap, 6px );
902 + border-radius: var( --os-dock-floating-radius, 18px );
845 903
846 904 /* Warmer, more saturated glass tint than vertical placements so
847 905 * the floating pill reads as a distinct piece of chrome rather
848 906 * than a section of the sidebar.
@@ -858,10 +916,10 @@
858 916 background-color: var(
859 917 --os-dock-floating-bg,
860 918 rgba( 22, 22, 26, 0.42 )
861 919 );
862 - backdrop-filter: blur( 28px ) saturate( 170% );
863 - -webkit-backdrop-filter: blur( 28px ) saturate( 170% );
920 + backdrop-filter: var( --os-dock-floating-backdrop, blur( 28px ) saturate( 170% ) );
921 + -webkit-backdrop-filter: var( --os-dock-floating-backdrop, blur( 28px ) saturate( 170% ) );
864 922
865 923 /*
866 924 * Hairline + inset highlight. These used to be hardcoded, which
867 925 * meant a theme could restyle every dock placement EXCEPT this
@@ -907,9 +965,9 @@
907 965 transform: translateX( -50% );
908 966 width: 4px;
909 967 height: 4px;
910 968 border-radius: 50%;
911 - background: var( --os-dock-item-outline, #fff );
969 + background: var( --os-dock-active-indicator, var( --os-dock-item-outline, #fff ) );
912 970 }
913 971
914 972 .os-dock[ data-os-dock-placement="bottom" ]
915 973 .os-dock__item--focused::before {
@@ -928,36 +986,33 @@
928 986 width: 6px;
929 987 height: 6px;
930 988 border-radius: 50%;
931 989 background: transparent;
932 - border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
990 + border: 1px solid var( --os-dock-active-indicator, var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) ) );
933 991 }
934 992
935 993 /*
936 - * Global "Show Desktop" indicator — set on `<body>` by the dock when
937 - * every live window on the active desktop is in the minimized state.
938 - * The bottom-dock pill picks up a soft theme-color ring so the user
939 - * has a global cue that the windows haven't disappeared, they're just
940 - * minimized. ArrowUp / a wallpaper click / clicking any dock tile
941 - * brings them back; without this the empty wallpaper looks identical
942 - * to a freshly-loaded desktop with no windows open.
994 + * "Show Desktop" — every live window on the active desktop minimized.
995 + *
996 + * The dock does not change when this happens. It used to: the bottom
997 + * pill and the vertical rails each picked up a 1px inset ring, on the
998 + * reasoning that an empty wallpaper otherwise looks identical to a
999 + * desktop that never had anything on it.
1000 + *
1001 + * The reasoning was sound and the ring was still the wrong answer. It
1002 + * outlined the whole dock — the one surface on screen that is always
1003 + * present and never the subject — to say something about windows that
1004 + * are not on the dock at all. Every tile that has minimized windows
1005 + * already says so, precisely and locally, by swapping its solid
1006 + * indicator dot for a hollow ring. That is the cue: it points at the
1007 + * tiles the windows are under, in the place the user will click to get
1008 + * them back.
1009 + *
1010 + * `body.os-show-desktop-active` is still set (see `src/dock.ts`) and
1011 + * still public, so a theme or plugin that wants a global cue can paint
1012 + * one. Core does not.
943 1013 */
944 -body.os-show-desktop-active
945 - .os-dock[ data-os-dock-placement="bottom" ] {
946 - box-shadow:
947 - inset 0 0 0 1px var( --wp-admin-theme-color, rgba( 255, 255, 255, 0.45 ) ),
948 - inset 0 1px 0 rgba( 255, 255, 255, 0.08 ),
949 - 0 10px 32px rgba( 0, 0, 0, 0.45 );
950 -}
951 1014
952 -/* Vertical docks get the same treatment along their inner edge. */
953 -body.os-show-desktop-active
954 - .os-dock[ data-os-dock-placement="left" ],
955 -body.os-show-desktop-active
956 - .os-dock[ data-os-dock-placement="right" ] {
957 - box-shadow: inset 0 0 0 1px var( --wp-admin-theme-color, rgba( 255, 255, 255, 0.45 ) );
958 -}
959 -
960 1015 /* "Open another" chip — floats above the tile's top-right corner so
961 1016 * it doesn't collide with neighbouring tiles in the horizontal row. */
962 1017 .os-dock[ data-os-dock-placement="bottom" ]
963 1018 .os-dock__item-new {
@@ -973,28 +1028,52 @@
973 1028
974 1029 /*
975 1030 * Bottom-dock inner wrappers — `__scroll` carries the menu tiles and
976 1031 * absorbs horizontal overflow; `__pinned` carries the system tiles and
977 - * stays anchored to the trailing edge. Padding-top on `__scroll` gives
978 - * the badge (top: -3px on the tile) room so `overflow-y: hidden`
979 - * doesn't crop it. `min-width: 0` lets the wrapper shrink below its
1032 + * stays anchored to the trailing edge.
1033 + *
1034 + * **The padding around `__scroll` is what keeps badges visible, and it
1035 + * has to be on both axes.** `overflow-x: auto` below cannot coexist
1036 + * with `overflow-y: visible`: per spec the visible axis computes to
1037 + * `auto`, so this wrapper is a scroll container in BOTH directions
1038 + * however it is authored, and anything a child hangs outside its
1039 + * padding box is clipped. A badge sits at `top: -3px; right: -3px` on
1040 + * its tile, and the LAST tile's edge is the wrapper's edge once the
1041 + * content is wide enough to scroll — which is why this showed up as
1042 + * "the badge on the last plugin looks cut off" rather than as a
1043 + * general problem. The inline padding is symmetric so the tile cluster
1044 + * stays centred in the pill. `min-width: 0` lets the wrapper shrink below its
980 1045 * content's natural width, so the dock pill's `max-width` is what
981 1046 * decides when scroll kicks in instead of the content forcing the
982 - * pill wider. `justify-content: center` keeps tiles balanced inside
983 - * the pill when content fits; when it overflows, scroll engages and
984 - * items extend past the visible area.
1047 + * pill wider. `justify-content: safe center` keeps tiles balanced inside
1048 + * the pill when content fits; when it overflows, the `safe` keyword
1049 + * ensures alignment falls back to `start` so `scrollLeft = 0` reveals the
1050 + * first item and all items remain scrollable without clipping. Bare
1051 + * `center` put the leading tiles at a negative offset, and a scroll
1052 + * container clamps `scrollLeft` at 0, so Dashboard and its neighbours
1053 + * were clipped with no way to scroll back to them. **The plain `start`
1054 + * above it is the fallback, not a leftover** — a browser that cannot
1055 + * parse the overflow-alignment keyword drops the `safe center`
1056 + * declaration and keeps that one, which left-aligns the tiles rather
1057 + * than hiding them. Deleting it restores the bug on those browsers.
985 1058 */
986 1059 .os-dock[ data-os-dock-placement="bottom" ] .os-dock__scroll {
987 1060 display: flex;
988 1061 flex-direction: row;
989 1062 align-items: center;
990 - justify-content: center;
991 - gap: 6px;
1063 + justify-content: start;
1064 + justify-content: var( --os-dock-floating-justify, safe center );
1065 + gap: var( --os-dock-floating-gap, 6px );
992 1066 flex: 1 1 auto;
993 1067 min-width: 0;
994 - padding-top: 4px;
995 - padding-bottom: 6px;
1068 + padding: var( --os-dock-floating-track-padding, 4px 4px 6px );
996 1069 overflow-x: auto;
1070 + /*
1071 + * Authored `visible`, computed `auto` — see the note above. Kept as
1072 + * the honest statement of intent: nothing here wants a vertical
1073 + * scrollbar, and `scrollbar-width: none` plus the padding is what
1074 + * makes that true in practice.
1075 + */
997 1076 overflow-y: visible;
998 1077 scrollbar-width: none;
999 1078 }
1000 1079
@@ -1005,57 +1084,67 @@
1005 1084 .os-dock[ data-os-dock-placement="bottom" ] .os-dock__pinned {
1006 1085 display: flex;
1007 1086 flex-direction: row;
1008 1087 align-items: center;
1009 - gap: 6px;
1088 + gap: var( --os-dock-floating-gap, 6px );
1010 1089 flex-shrink: 0;
1011 1090 /* Match the scroll wrapper's badge clearance so system tiles sit
1012 1091 * on the same vertical baseline as the menu tiles. */
1013 - padding-top: 4px;
1014 - padding-bottom: 6px;
1092 + padding: var( --os-dock-floating-pinned-padding, 4px 0 6px );
1093 + /* A well the system tiles sit in, when a theme draws one. */
1094 + background: var( --os-dock-pinned-bg, transparent );
1095 + box-shadow: var( --os-dock-pinned-shadow, none );
1015 1096 }
1016 1097
1017 -/* Separator — vertical hairline between menu tiles and system tiles.
1018 - * Lives at the leading edge of `__pinned`; flex flow handles the
1098 +/* Separator — the line between two groups of tiles. The system one
1099 + * lives at the leading edge of `__pinned` (flex flow handles the
1019 1100 * "pinned at the trailing edge" placement, no margin-inline-start:
1020 - * auto needed. The `--group` variant keeps the hairline styling for
1021 - * the inline core→plugin boundary inside `__scroll` and sits where
1022 - * inserted. */
1101 + * auto needed); the `--group` one sits inline inside `__scroll` where
1102 + * it was inserted. Both resolve through this single rule — see the
1103 + * note on the vertical placements above for why there is only one. */
1023 1104 .os-dock[ data-os-dock-placement="bottom" ]
1024 1105 .os-dock__separator {
1025 - width: 1px;
1106 + position: relative;
1107 + width: var( --os-dock-separator-width, 2px );
1026 1108 /* Fixed height — `60%` collapses against the floating pill's
1027 - intrinsic height, leaving the hairline effectively invisible.
1028 - 28px matches the dock tile inner glyph area. */
1029 - height: 28px;
1109 + intrinsic height, leaving the line effectively invisible. */
1110 + height: var( --os-dock-separator-height, 34px );
1030 1111 /* Symmetric horizontal margin so the divider sits centered in the
1031 1112 * inter-tile gap: the 6px flex gap on each side (`__scroll`→`__pinned`
1032 - * on the left, `__pinned`'s own gap on the right) plus an equal 6px
1113 + * on the left, `__pinned`'s own gap on the right) plus an equal
1033 1114 * margin per side balances out. An asymmetric margin here nudges the
1034 1115 * divider — and the whole menu cluster with it — off the pill's
1035 - * center. */
1036 - margin: auto 6px;
1037 - background: var( --os-dock-border );
1116 + * center. Wider than the tile gap on purpose: the clusters need to
1117 + * read as groups before the line between them means anything. */
1118 + margin: auto var( --os-dock-separator-margin, 14px );
1119 + border-radius: 2px;
1120 + /*
1121 + * The glowing seam by default. A theme drawing an etched groove
1122 + * instead answers `--os-dock-separator-bg` with a flat colour and
1123 + * `--os-dock-separator-shadow` with the highlight beside it.
1124 + */
1125 + background: var(
1126 + --os-dock-separator-bg,
1127 + linear-gradient(
1128 + to bottom,
1129 + transparent 0%,
1130 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 28%,
1131 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 72%,
1132 + transparent 100%
1133 + )
1134 + );
1135 + box-shadow: var(
1136 + --os-dock-separator-shadow,
1137 + 0 0 10px
1138 + color-mix(
1139 + in srgb,
1140 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 45%,
1141 + transparent
1142 + )
1143 + );
1038 1144 flex-shrink: 0;
1039 1145 }
1040 1146
1041 -/*
1042 - * Group separator — sits inline between the core cluster and the
1043 - * plugin cluster. Brighter than the menu-vs-system separator because
1044 - * it carries user meaning ("this is where your installed apps
1045 - * begin"), and flanked by extra gap so the boundary reads at a
1046 - * glance rather than disappearing into the dock tint.
1047 - */
1048 -.os-dock[ data-os-dock-placement="bottom" ]
1049 - .os-dock__separator--group {
1050 - width: 1px;
1051 - height: 60%;
1052 - margin: auto 10px;
1053 - margin-inline-start: 10px;
1054 - background: rgba( 255, 255, 255, 0.22 );
1055 - flex-shrink: 0;
1056 -}
1057 -
1058 1147 /* Tooltip — above the hovered tile. JS writes the horizontal center
1059 1148 * + a top near the tile's top edge; CSS translates the tooltip up by
1060 1149 * its own height so it clears the tile. */
1061 1150 .os-dock__tooltip--above {
@@ -1095,5 +1184,305 @@
1095 1184 }
1096 1185
1097 1186 .os-dock__item:not( .os-dock__item--system ):active {
1098 1187 cursor: grabbing;
1188 +}
1189 +
1190 +/* ------------------------------------------------------------------
1191 + * The way out — `Exit OpenStation`
1192 + *
1193 + * Every other system tile opens something you can close again. This
1194 + * one leaves the desktop, and with the admin bar hidden by default it
1195 + * is the only route back to classic admin. Drawn like its neighbours
1196 + * it read as one more launcher, which is the same "some tiles do a
1197 + * different kind of thing" confusion the single dock set out to fix,
1198 + * just moved to the other end of the rail.
1199 + *
1200 + * Three signals, none of them colour. Danger red would overstate it
1201 + * (nothing is destroyed, the session is saved and the desktop is one
1202 + * click away) and Pulse is already spent on the seam:
1203 + *
1204 + * 1. **Last, always.** `order: 1` rather than registration order —
1205 + * native-window tiles from plugins sync in after boot and would
1206 + * otherwise land behind it.
1207 + * 2. **Its own gap**, wider than the rail's 6px, so it reads as
1208 + * sitting outside the set rather than at the end of it. A second
1209 + * divider would have said this too, and would have cost the rail
1210 + * the one-structural-line rule it just earned.
1211 + * 3. **A different silhouette** — a ring rather than a plate. Shape
1212 + * survives every theme, every dock texture, greyscale and a
1213 + * colour-blind reading of the rail; a tint survives none of them
1214 + * reliably.
1215 + *
1216 + * And it leans toward the edge it leads to on hover instead of lifting
1217 + * toward the pointer, because every other tile's lift means "I will
1218 + * come to you" and this one means the opposite.
1219 + *
1220 + * Keyed on the tile id, the same idiom the Recycle Bin badge uses
1221 + * above. `os-exit` is frozen in `src/exit-openstation.ts`
1222 + * (`EXIT_OPENSTATION_TILE_ID`) and `dock-exit-tile.test.ts` pins the
1223 + * attribute this selector depends on.
1224 + * ------------------------------------------------------------------ */
1225 +
1226 +.os-dock__item[ data-system-id="os-exit" ] {
1227 + order: 1;
1228 +}
1229 +
1230 +.os-dock__item[ data-system-id="os-exit" ] .os-dock__item-primary {
1231 + border-radius: var( --os-dock-exit-radius, 50% );
1232 + background-color: transparent;
1233 + box-shadow: inset 0 0 0 1px var(
1234 + --os-dock-exit-ring,
1235 + var( --os-dock-border, rgba( 255, 255, 255, 0.18 ) )
1236 + );
1237 + color: var( --os-dock-exit-icon, rgba( 255, 255, 255, 0.55 ) );
1238 +}
1239 +
1240 +/*
1241 + * The gap runs along the rail's own axis, so it follows the placement:
1242 + * inline for the horizontal pill, block for the vertical pillars.
1243 + */
1244 +.os-dock[ data-os-dock-placement="bottom" ]
1245 + .os-dock__item[ data-system-id="os-exit" ] {
1246 + margin-inline-start: var( --os-dock-exit-gap, 10px );
1247 +}
1248 +
1249 +.os-dock[ data-os-dock-placement="left" ]
1250 + .os-dock__item[ data-system-id="os-exit" ],
1251 +.os-dock[ data-os-dock-placement="right" ]
1252 + .os-dock__item[ data-system-id="os-exit" ] {
1253 + margin-block-start: 8px;
1254 +}
1255 +
1256 +/*
1257 + * Hover: the ring closes up and the tile moves TOWARD its edge. The
1258 + * generic tile rule scales up by 1.1, so these have to out-specify it
1259 + * rather than sit alongside it.
1260 + */
1261 +.os-dock__item[ data-system-id="os-exit" ] .os-dock__item-primary:hover,
1262 +.os-dock__item[ data-system-id="os-exit" ]
1263 + .os-dock__item-primary:focus-visible {
1264 + background-color: transparent;
1265 + box-shadow: inset 0 0 0 1px var(
1266 + --os-dock-exit-ring-hover,
1267 + var( --os-dock-item-outline, rgba( 255, 255, 255, 0.7 ) )
1268 + );
1269 + color: var( --os-dock-icon-color-hover, var( --os-ui-fg-on-accent, #fff ) );
1270 +}
1271 +
1272 +.os-dock[ data-os-dock-placement="bottom" ]
1273 + .os-dock__item[ data-system-id="os-exit" ]
1274 + .os-dock__item-primary:hover {
1275 + transform: var( --os-dock-exit-hover-transform, translateY( 2px ) );
1276 +}
1277 +
1278 +.os-dock[ data-os-dock-placement="left" ]
1279 + .os-dock__item[ data-system-id="os-exit" ]
1280 + .os-dock__item-primary:hover {
1281 + transform: translateX( -2px );
1282 +}
1283 +
1284 +.os-dock[ data-os-dock-placement="right" ]
1285 + .os-dock__item[ data-system-id="os-exit" ]
1286 + .os-dock__item-primary:hover {
1287 + transform: translateX( 2px );
1288 +}
1289 +
1290 +/* The press keeps the shared cue: every tile in the rail dips on
1291 + * :active, and this one should not feel unresponsive by comparison. */
1292 +.os-dock__item[ data-system-id="os-exit" ] .os-dock__item-primary:active {
1293 + transform: scale( 0.95 );
1294 +}
1295 +
1296 +@media ( prefers-reduced-motion: reduce ) {
1297 + .os-dock[ data-os-dock-placement="bottom" ]
1298 + .os-dock__item[ data-system-id="os-exit" ]
1299 + .os-dock__item-primary:hover,
1300 + .os-dock[ data-os-dock-placement="left" ]
1301 + .os-dock__item[ data-system-id="os-exit" ]
1302 + .os-dock__item-primary:hover,
1303 + .os-dock[ data-os-dock-placement="right" ]
1304 + .os-dock__item[ data-system-id="os-exit" ]
1305 + .os-dock__item-primary:hover {
1306 + transform: none;
1307 + }
1308 +}
1309 +
1310 +/* ------------------------------------------------------------------
1311 + * Dynamic dock behavior — OpenStation Preferences → Appearance →
1312 + * Desktop layout, persisted as `dockBehavior` (the dock) and
1313 + * `sideDockBehavior` (the Split sidebar) and worn by each rail as
1314 + * `data-os-dock-behavior` — an attribute PER RAIL rather than a body
1315 + * class, because Split's two rails answer independently. PHP stamps
1316 + * the dock on first paint; the apply pass and `src/dock-behavior.ts`
1317 + * re-stamp on every change and rebuild. `static` is the absence of
1318 + * rules.
1319 + *
1320 + * A dynamic rail that is not `os-dock--revealed` is PARKED: it
1321 + * collapses into a thin indicator line hugging its edge — the iOS
1322 + * home indicator, one line that says "there is a dock here" — and
1323 + * expands back into the full rail when the pointer comes for it.
1324 + * `src/dock-behavior.ts` owns the flip (a full-width edge zone, the
1325 + * rail's own box, its flyouts, keyboard focus) and runs it through
1326 + * the View Transitions API, so the line morphs into the pill and
1327 + * back; the rules below only describe the two resting states and
1328 + * tune the morph. There is deliberately no `:hover` here — a state
1329 + * CSS flipped on its own would jump instead of morphing.
1330 + *
1331 + * The line is the rail itself, not a separate element: same node,
1332 + * same `view-transition-name`, which is what lets the browser animate
1333 + * one box into the other. Its children are hidden while parked so
1334 + * the line is a line. A side rail leaves the flex row for the
1335 + * duration, so the area grows to the full width and the rail rides
1336 + * over it when summoned; the work area reserves nothing for a
1337 + * dynamic rail (`src/work-area/index.ts`).
1338 + * ------------------------------------------------------------------ */
1339 +
1340 +/*
1341 + * Side rails overlay the area in both states — the flex row must not
1342 + * shrink for a rail that is only sometimes there.
1343 + */
1344 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ],
1345 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ] {
1346 + position: absolute;
1347 + inset-block: 0;
1348 +}
1349 +
1350 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ] {
1351 + inset-inline-start: 0;
1352 +}
1353 +
1354 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ] {
1355 + inset-inline-end: 0;
1356 +}
1357 +
1358 +/*
1359 + * Parked — the indicator line. Everything that makes the pill a
1360 + * pill (padding, border, blur, texture, tile gap) comes off, and the
1361 + * box is the line's size. `overflow: hidden` clips whatever a tile
1362 + * paints past its box during the morph.
1363 + */
1364 +.os-dock[ data-os-dock-behavior="dynamic" ]:not( .os-dock--revealed ) {
1365 + min-width: 0;
1366 + min-height: 0;
1367 + max-width: none;
1368 + padding: 0;
1369 + gap: 0;
1370 + border: 0;
1371 + border-radius: 999px;
1372 + overflow: hidden;
1373 + background-color: var(
1374 + --os-dock-indicator-bg,
1375 + var( --os-dock-item-outline, rgba( 255, 255, 255, 0.55 ) )
1376 + );
1377 + background-image: none;
1378 + backdrop-filter: none;
1379 + -webkit-backdrop-filter: none;
1380 + box-shadow: none;
1381 + cursor: pointer;
1382 +}
1383 +
1384 +.os-dock[ data-os-dock-behavior="dynamic" ]:not( .os-dock--revealed ) > * {
1385 + display: none;
1386 +}
1387 +
1388 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="bottom" ]:not( .os-dock--revealed ) {
1389 + width: var( --os-dock-indicator-length, 180px );
1390 + height: var( --os-dock-indicator-thickness, 5px );
1391 + bottom: 8px;
1392 +}
1393 +
1394 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ]:not( .os-dock--revealed ),
1395 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ]:not( .os-dock--revealed ) {
1396 + width: var( --os-dock-indicator-thickness, 5px );
1397 + height: var( --os-dock-indicator-length, 180px );
1398 + inset-block: calc( 50% - var( --os-dock-indicator-length, 180px ) / 2 );
1399 +}
1400 +
1401 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ]:not( .os-dock--revealed ) {
1402 + inset-inline-start: 8px;
1403 +}
1404 +
1405 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ]:not( .os-dock--revealed ) {
1406 + inset-inline-end: 8px;
1407 +}
1408 +
1409 +/*
1410 + * The morph. With the API, `src/dock-behavior.ts` names the rail for
1411 + * the duration of one flip and the browser animates the old box into
1412 + * the new one; the rail's contents cross-fade underneath. The root is
1413 + * opted out of the snapshot so the rest of the desktop keeps
1414 + * running while the line becomes a pill — a frozen page for 260ms
1415 + * on every reveal would be the animation costing more than it
1416 + * gives. Without the API, the properties that can transition do.
1417 + */
1418 +@supports ( view-transition-name: none ) {
1419 + html.os-dock-vt {
1420 + view-transition-name: none;
1421 + }
1422 +
1423 + html.os-dock-vt::view-transition-old( root ),
1424 + html.os-dock-vt::view-transition-new( root ) {
1425 + animation: none;
1426 + }
1427 +
1428 + /*
1429 + * The transition layer must not catch the pointer. By default it
1430 + * does — the whole page reads as inert until the morph settles —
1431 + * and with the root opted out of the snapshot that is worse than
1432 + * pointless: the live desktop is right there under a layer that
1433 + * swallows every mouseup and every pointerenter for 260ms. The
1434 + * behavior module already holds a park back while a button is
1435 + * down; this is the other half, so a hover that lands on a tile
1436 + * while the rail is still morphing out starts its hover-intent
1437 + * timer instead of waiting for the next pointer movement.
1438 + */
1439 + html.os-dock-vt::view-transition {
1440 + pointer-events: none;
1441 + }
1442 +
1443 + ::view-transition-group( os-dock-os-dock ),
1444 + ::view-transition-group( os-dock-os-side-dock ) {
1445 + animation-duration: 260ms;
1446 + animation-timing-function: cubic-bezier( 0.22, 1, 0.36, 1 );
1447 + }
1448 +
1449 + ::view-transition-old( os-dock-os-dock ),
1450 + ::view-transition-old( os-dock-os-side-dock ) {
1451 + animation: os-dock-vt-out 120ms ease-out both;
1452 + }
1453 +
1454 + ::view-transition-new( os-dock-os-dock ),
1455 + ::view-transition-new( os-dock-os-side-dock ) {
1456 + animation: os-dock-vt-in 180ms ease-out 80ms both;
1457 + }
1458 +
1459 + @keyframes os-dock-vt-out {
1460 + to {
1461 + opacity: 0;
1462 + }
1463 + }
1464 +
1465 + @keyframes os-dock-vt-in {
1466 + from {
1467 + opacity: 0;
1468 + }
1469 + }
1470 +}
1471 +
1472 +@supports not ( view-transition-name: none ) {
1473 + .os-dock[ data-os-dock-behavior="dynamic" ] {
1474 + transition:
1475 + width 240ms ease,
1476 + height 240ms ease,
1477 + opacity 240ms ease,
1478 + border-radius 240ms ease,
1479 + bottom 240ms ease,
1480 + inset-block 240ms ease;
1481 + }
1482 +}
1483 +
1484 +@media ( prefers-reduced-motion: reduce ) {
1485 + .os-dock[ data-os-dock-behavior="dynamic" ] {
1486 + transition: none;
1487 + }
1099 1488 }