PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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
← All changes | assets/css/desktop.css +678 -187 1.0.11.1.10 View file →
@@ -156,8 +156,27 @@
156 156 inset-inline-end: 0 !important;
157 157 }
158 158
159 159 /*
160 + * No top-level item may be taller than the bar.
161 + *
162 + * Core lays the two item groups out as floats, and a float that
163 + * overflows the 32px bar overflows invisibly. A host may lay a group
164 + * out as a flex row instead — WordPress.com's Debug Bar does, to
165 + * order its own item first — and then every item stretches to the
166 + * tallest one and the group's background paints the difference under
167 + * the windows' title bars. Our own items were that tallest one once
168 + * (an `inline-flex` item on a 32px line box grew to 37px; fixed at
169 + * the source in `includes/admin-bar.php`), but any item from any
170 + * plugin can be, so the cap lives here: the bar's own height token,
171 + * which is also the 46px mobile bar. Submenus are absolutely
172 + * positioned and unaffected.
173 + */
174 +body.os-active #wpadminbar .ab-top-menu > li {
175 + max-height: var(--wp-admin--admin-bar--height, 32px);
176 +}
177 +
178 +/*
160 179 * Admin-bar presentation modes — OS Settings → Appearance → Admin bar,
161 180 * persisted as `adminBarMode` and emitted as a
162 181 * `os-admin-bar-<mode>` body class by PHP on first paint and
163 182 * re-written by the shell's apply pass on every change.
@@ -166,9 +185,11 @@
166 185 * starts below the bar. The two modes below both take the bar out of
167 186 * the shell's way, so the shell reclaims the full viewport in each.
168 187 *
169 188 * Every selector here carries two body classes, so it outranks the
170 - * one-class pin rule above without needing to escalate anything.
189 + * one-class pin rule above on specificity; the one property the pin
190 + * marks `!important` (`inset-block-start`) is re-declared `!important`
191 + * where a mode moves the bar, and wins the same way.
171 192 */
172 193
173 194 /*
174 195 * Dynamic — the bar parks off the top edge leaving a peek strip, and
@@ -173,25 +194,50 @@
173 194 /*
174 195 * Dynamic — the bar parks off the top edge leaving a peek strip, and
175 196 * slides back in when the pointer reaches the top of the viewport or
176 197 * something inside it takes keyboard focus. The classic Windows
177 - * auto-hide taskbar, and the reason `transform` is the right tool: a
178 - * transformed element keeps hit-testing at its PAINTED position, so
179 - * the parked remainder stops catching the pointer the moment it goes
180 - * off-screen. What DOES catch it is the peek strip plus the reveal
181 - * zone added below.
198 + * auto-hide taskbar.
199 + *
200 + * It parks by moving its `inset-block-start`, NOT by a `transform`,
201 + * and that is load-bearing. A transformed element becomes the
202 + * containing block for every `position: fixed` descendant, and the
203 + * bar has descendants like that which it does not own: the
204 + * WordPress.com notifications panel is `position: fixed; top: 32px;
205 + * bottom: 0` inside `#wp-admin-bar-notes`. Measured against a
206 + * transformed 32px bar instead of the viewport, that panel computes to
207 + * zero height — the button "did nothing" in this mode and worked in
208 + * `static`. `translate`, `filter`, `perspective`, `contain` and
209 + * `will-change: transform` all create the same containing block;
210 + * `tests/vitest/admin-bar-dynamic.test.ts` keeps every one of them off
211 + * this rule. Moving `inset-block-start` hit-tests the same way — the
212 + * part above the viewport is simply outside it — so what catches the
213 + * pointer is still only the peek strip plus the reveal zone below.
214 + *
215 + * The pin above sets `inset-block-start: 0 !important`; these carry
216 + * `!important` too and win on specificity. The bar's height is Core's
217 + * own token, so the parked offset follows the 46px mobile bar as well.
182 218 */
183 219 body.os-active.os-admin-bar-dynamic #wpadminbar {
184 - transform: translateY(
185 - calc(-100% + var(--os-admin-bar-peek, 4px))
186 - );
187 - transition: transform 180ms ease;
220 + /* One number for the slide; the reveal zone below waits on it. */
221 + --os-admin-bar-slide: 180ms;
222 + inset-block-start: calc(
223 + var(--os-admin-bar-peek, 4px) - var(--wp-admin--admin-bar--height, 32px)
224 + ) !important;
225 + transition: inset-block-start var(--os-admin-bar-slide, 180ms) ease;
188 226 }
189 227
190 228 body.os-active.os-admin-bar-dynamic #wpadminbar:hover,
191 229 body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within,
192 -body.os-active.os-admin-bar-dynamic #wpadminbar:active {
193 - transform: translateY(0);
230 +body.os-active.os-admin-bar-dynamic #wpadminbar:active,
231 +/*
232 + * …and while the notch is being used. The notch hangs from the same
233 + * edge and steps down to sit under the bar when it appears (see
234 + * notch.css), so letting the bar retract out from over it mid-reach
235 + * would drop the notch back up under the pointer.
236 + */
237 +body.os-active.os-admin-bar-dynamic:has( .os-notch:hover ) #wpadminbar,
238 +body.os-active.os-admin-bar-dynamic:has( .os-notch:focus-visible ) #wpadminbar {
239 + inset-block-start: 0 !important;
194 240 }
195 241
196 242 /*
197 243 * Reveal zone — an invisible extension of the bar's hit area, hanging
@@ -211,8 +257,25 @@
211 257 position: absolute;
212 258 inset-inline: 0;
213 259 inset-block-start: 100%;
214 260 height: var(--os-admin-bar-reveal-zone, 16px);
261 + /*
262 + * The zone changes size only once the bar has come to rest — a
263 + * step, not a slide, held back for the length of the bar's own
264 + * slide. That delay is what keeps the reveal stable. Collapsed the
265 + * instant hover began, the zone was gone while the bar was still
266 + * on its way down: a pointer resting in the band the zone had
267 + * covered was outside the bar's box for the rest of the slide, so
268 + * hover dropped, the bar turned back, the zone returned under the
269 + * pointer, hover resumed — the bar flickered up and down for as
270 + * long as the pointer stayed near the edge. Held for the slide,
271 + * the zone still hangs off the moving bar and the two together
272 + * always cover the pointer that tripped it; by the time the zone
273 + * goes, the bar's own box is over that pointer. The same delay on
274 + * the way back keeps the zone from reappearing under a pointer
275 + * that just left the bar while the bar is still retracting.
276 + */
277 + transition: height 0s linear var(--os-admin-bar-slide, 180ms);
215 278 }
216 279
217 280 /*
218 281 * ...but only while the bar is parked. Left standing once the bar is
@@ -220,9 +283,10 @@
220 283 * clicks on whatever window is up there.
221 284 *
222 285 * Collapsing it is also what keeps the reveal stable: the pointer
223 286 * that tripped the zone is, post-reveal, inside the bar's real box,
224 - * so `:hover` holds without the zone's help.
287 + * so `:hover` holds without the zone's help — and it collapses only
288 + * once the bar is there (the transition above).
225 289 */
226 290 body.os-active.os-admin-bar-dynamic #wpadminbar:hover::after,
227 291 body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within::after,
228 292 body.os-active.os-admin-bar-dynamic #wpadminbar:active::after {
@@ -242,9 +306,11 @@
242 306 }
243 307 }
244 308
245 309 @media (prefers-reduced-motion: reduce) {
246 - body.os-active.os-admin-bar-dynamic #wpadminbar {
310 + body.os-active.os-admin-bar-dynamic #wpadminbar,
311 + body.os-active.os-admin-bar-dynamic #wpadminbar::after {
312 + /* The bar jumps, so the zone has nothing to wait for. */
247 313 transition: none;
248 314 }
249 315 }
250 316
@@ -328,12 +394,26 @@
328 394 /*
329 395 * Desktop shell — fills the viewport below the admin bar.
330 396 * Uses position: fixed so it's unaffected by any containing-block
331 397 * quirks in #wpwrap/#wpcontent.
398 + *
399 + * The inset is the bar's MEASURED bottom edge, `--os-admin-bar-height`
400 + * (written on <html> by `src/admin-bar-height.ts`), with Core's own
401 + * token behind it for the first paint. Core's token is Core's promise
402 + * about Core's bar; a host can make the bar taller, give it a border
403 + * or push it down under a strip of its own — WordPress.com's staff
404 + * debug chrome does — and the shell then started under the bar by
405 + * exactly the difference. The measured token is absent whenever the
406 + * bar has no box at the top edge (hidden, mobile, solo, fullscreen,
407 + * parked in the dynamic mode), so every mode that rebinds Core's
408 + * token below still resolves through it.
332 409 */
333 410 .os-shell {
334 411 position: fixed;
335 - inset-block-start: var(--wp-admin--admin-bar--height, 32px);
412 + inset-block-start: var(
413 + --os-admin-bar-height,
414 + var(--wp-admin--admin-bar--height, 32px)
415 + );
336 416 inset-inline-start: 0;
337 417 inset-inline-end: 0;
338 418 inset-block-end: 0;
339 419 display: flex;
@@ -548,9 +628,12 @@
548 628
549 629 /* Below 783px WP collapses the admin bar to 46px. */
550 630 @media screen and (max-width: 782px) {
551 631 .os-shell {
552 - inset-block-start: var(--wp-admin--admin-bar--height, 46px);
632 + inset-block-start: var(
633 + --os-admin-bar-height,
634 + var(--wp-admin--admin-bar--height, 46px)
635 + );
553 636 }
554 637 }
555 638
556 639 /*
@@ -586,11 +669,17 @@
586 669 * because the wallpaper renders in the shell-level layer behind
587 670 * both this area and the dock. Stacking context via position: relative
588 671 * ensures children (windows) sit above the wallpaper.
589 672 *
590 - * `padding-bottom` reserves room for the floating bottom dock pill
591 - * that every layout (Classic, Unified, Spatial) puts at the bottom
592 - * — 40px icons + 16px padding + 12px gap below + 8px breathing room.
673 + * The padding reserves the band the floating bottom dock pill covers.
674 + * It reads the work-area insets `src/work-area/index.ts` writes on
675 + * `#os-shell` from the pill's LIVE geometry, so it is 0 with a side
676 + * dock (which is a flex sibling and already narrows the area) and
677 + * tracks the dock-size preference with the pill at the bottom. The
678 + * `80px` literal is the pre-measurement floor: 40px tile + 16px pill
679 + * padding + 12px gap below + 8px breathing room, what the rule
680 + * hardcoded before the work area existed, in force until the shell
681 + * has measured once.
593 682 */
594 683 .os-area {
595 684 flex: 1;
596 685 position: relative;
@@ -595,9 +684,12 @@
595 684 flex: 1;
596 685 position: relative;
597 686 overflow: hidden;
598 687 background: transparent;
599 - padding-bottom: 80px;
688 + padding-top: var( --os-work-area-inset-top, 0px );
689 + padding-right: var( --os-work-area-inset-right, 0px );
690 + padding-bottom: var( --os-work-area-inset-bottom, 80px );
691 + padding-left: var( --os-work-area-inset-left, 0px );
600 692 transition: opacity 180ms ease;
601 693 }
602 694
603 695 /*
@@ -669,12 +761,23 @@
669 761 * --------------------------------------------------------------- */
670 762
671 763 .os-icons {
672 764 position: absolute;
673 - inset-block-start: 16px;
674 - inset-block-end: 16px;
675 - inset-inline-start: 16px;
676 - inset-inline-end: 16px;
765 + /*
766 + * 16px of gutter inside the WORK AREA, not inside the desktop
767 + * area: an absolutely positioned box ignores its parent's
768 + * padding, so without the insets the grid's last row sat under
769 + * the bottom dock pill. Same tokens `.os-area` reserves with.
770 + *
771 + * Physical `left` / `right`, not the logical pair: the insets are
772 + * measured from rects, so `-left` is the visual left whichever
773 + * way the locale reads. The gutter is 16px on both sides, so
774 + * nothing is lost by going physical here.
775 + */
776 + top: calc( var( --os-work-area-inset-top, 0px ) + 16px );
777 + bottom: calc( var( --os-work-area-inset-bottom, 80px ) + 16px );
778 + left: calc( var( --os-work-area-inset-left, 0px ) + 16px );
779 + right: calc( var( --os-work-area-inset-right, 0px ) + 16px );
677 780 display: grid;
678 781 /*
679 782 * Column-major flow: icons fill the first column top-to-bottom,
680 783 * then start the next column. Matches the macOS / Finder
@@ -824,148 +927,8 @@
824 927 word-break: break-word;
825 928 }
826 929
827 930 /* ---------------------------------------------------------------
828 - * Sticky notes — Gutenberg `wp_guideline` artifacts tagged with the
829 - * sticky guideline type. Notes sit above wallpaper icons/widgets and
830 - * below windows, so they behave like desktop objects rather than app
831 - * chrome.
832 - * --------------------------------------------------------------- */
833 -
834 -.os-sticky-notes {
835 - position: absolute;
836 - inset: 0;
837 - z-index: 2;
838 - pointer-events: none;
839 - transition:
840 - opacity 0.22s ease,
841 - transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
842 -}
843 -
844 -.os-area--overview .os-sticky-notes {
845 - opacity: 0;
846 - transform: translateY( 12px );
847 - pointer-events: none;
848 -}
849 -
850 -.os-sticky-note {
851 - position: absolute;
852 - display: flex;
853 - flex-direction: column;
854 - pointer-events: auto;
855 - background: #fff09a;
856 - color: #2a2513;
857 - border: 1px solid rgba( 84, 67, 0, 0.24 );
858 - border-radius: 4px;
859 - box-shadow:
860 - 0 12px 28px rgba( 0, 0, 0, 0.22 ),
861 - 0 2px 5px rgba( 0, 0, 0, 0.16 ),
862 - inset 0 1px 0 rgba( 255, 255, 255, 0.55 );
863 - overflow: hidden;
864 - resize: both;
865 -}
866 -
867 -.os-sticky-note--dragging {
868 - opacity: 0.92;
869 - resize: none;
870 - user-select: none;
871 -}
872 -
873 -.os-sticky-note__header {
874 - display: flex;
875 - align-items: center;
876 - gap: 5px;
877 - min-height: 30px;
878 - padding: 4px 6px 4px 8px;
879 - background: rgba( 82, 64, 0, 0.06 );
880 - border-bottom: 1px solid rgba( 82, 64, 0, 0.14 );
881 - cursor: grab;
882 - touch-action: none;
883 - user-select: none;
884 -}
885 -
886 -.os-sticky-note--dragging .os-sticky-note__header {
887 - cursor: grabbing;
888 -}
889 -
890 -.os-sticky-note__grip {
891 - flex: 0 0 auto;
892 - width: 8px;
893 - height: 14px;
894 - background-image: radial-gradient(
895 - circle,
896 - rgba( 60, 48, 0, 0.5 ) 1.2px,
897 - transparent 1.5px
898 - );
899 - background-size: 4px 4px;
900 - background-position: 0 1px;
901 - background-repeat: space;
902 - opacity: 0.42;
903 -}
904 -
905 -.os-sticky-note__title {
906 - flex: 1;
907 - min-width: 0;
908 - font-size: 11px;
909 - font-weight: 650;
910 - overflow: hidden;
911 - text-overflow: ellipsis;
912 - white-space: nowrap;
913 -}
914 -
915 -.os-sticky-note__status {
916 - flex: 0 0 auto;
917 -}
918 -
919 -.os-sticky-note os-window-button {
920 - --os-ui-btn-color: rgba( 42, 37, 19, 0.7 );
921 - --os-ui-btn-color-hover: #2a2513;
922 - --os-ui-btn-bg-hover: rgba( 42, 37, 19, 0.1 );
923 - --os-ui-btn-bg-active: rgba( 42, 37, 19, 0.16 );
924 - --os-ui-btn-danger-hover: rgba( 214, 54, 56, 0.18 );
925 - --os-ui-btn-outline: var( --wp-admin-theme-color, #2271b1 );
926 -}
927 -
928 -.os-sticky-note__open.is-disabled {
929 - opacity: 0.34;
930 - pointer-events: none;
931 -}
932 -
933 -.os-sticky-note__editor {
934 - flex: 1;
935 - min-height: 0;
936 - --os-window-bg: transparent;
937 - --os-ui-border: transparent;
938 - --os-ui-fg: #2a2513;
939 - --os-ui-fg-muted: rgba( 42, 37, 19, 0.62 );
940 - font-family:
941 - -apple-system,
942 - BlinkMacSystemFont,
943 - "Segoe UI",
944 - sans-serif;
945 -}
946 -
947 -.os-sticky-note__editor::part(textarea) {
948 - height: 100%;
949 - min-height: 100%;
950 - padding: 10px 12px 12px;
951 - background: transparent;
952 - border: 0;
953 - border-radius: 0;
954 - box-shadow: none;
955 - color: #2a2513;
956 - font-size: 13px;
957 - line-height: 1.4;
958 - resize: none;
959 -}
960 -
961 -.os-sticky-note__editor::part(textarea):hover,
962 -.os-sticky-note__editor::part(textarea):focus-visible {
963 - border-color: transparent;
964 - box-shadow: none;
965 -}
966 -
967 -/* ---------------------------------------------------------------
968 931 * Widgets column — right-edge glass strip that paints above the
969 932 * wallpaper but beneath windows. Hosts stacked widget cards and a
970 933 * trailing "Add widget" tile. Interactive only on its own cards +
971 934 * the add tile; every other pixel is pointer-transparent so window
@@ -974,10 +937,17 @@
974 937
975 938 .os-widgets {
976 939 border-radius: 14px;
977 940 position: absolute;
978 - top: 16px;
979 - bottom: 16px;
941 + /*
942 + * Inside the work area vertically — see `.os-icons` for why the
943 + * insets are here. The inline edge stays a plain 16px: the column
944 + * hugs the trailing side, which is a logical choice, while the
945 + * side insets are physical; and a side rail never claims one
946 + * anyway (it is a flex sibling, the area is already narrower).
947 + */
948 + top: calc( var( --os-work-area-inset-top, 0px ) + 16px );
949 + bottom: calc( var( --os-work-area-inset-bottom, 80px ) + 16px );
980 950 inset-inline-end: 16px;
981 951 width: 320px;
982 952 display: flex;
983 953 flex-direction: column;
@@ -1029,10 +999,16 @@
1029 999 * spills past the card's rounded bottom edge. */
1030 1000 display: flex;
1031 1001 flex-direction: column;
1032 1002 pointer-events: auto;
1033 - background-color: rgba( 20, 20, 22, 0.55 );
1034 1003 /*
1004 + * The glass is also `--os-ui-color-surface`, the one name in the
1005 + * widget contract that describes the card itself. Declared in
1006 + * `variables.css` so a widget reading it and the card painting it
1007 + * cannot drift apart.
1008 + */
1009 + background-color: var( --os-ui-color-surface, rgba( 20, 20, 22, 0.55 ) );
1010 + /*
1035 1011 * WIDGET texture slot — layered over the frosted card colour, so a
1036 1012 * translucent texture still gets the blur underneath.
1037 1013 */
1038 1014 background-image: var( --os-widget-image, none );
@@ -1073,10 +1049,12 @@
1073 1049 .os-widgets__card-close {
1074 1050 position: absolute;
1075 1051 top: 6px;
1076 1052 inset-inline-end: 6px;
1077 - width: 22px;
1078 - height: 22px;
1053 + /* 24px is the WCAG 2.2 SC 2.5.8 floor. The glyph inside is unchanged;
1054 + only the target grows. */
1055 + width: 24px;
1056 + height: 24px;
1079 1057 display: flex;
1080 1058 align-items: center;
1081 1059 justify-content: center;
1082 1060 padding: 0;
@@ -1161,10 +1139,10 @@
1161 1139 corner-absolute — reset position + always-visible opacity. */
1162 1140 .os-widgets__chrome .os-widgets__card-close {
1163 1141 position: static;
1164 1142 opacity: 0.7;
1165 - width: 20px;
1166 - height: 20px;
1143 + width: 24px;
1144 + height: 24px;
1167 1145 }
1168 1146
1169 1147 .os-widgets__chrome .os-widgets__card-close:hover,
1170 1148 .os-widgets__chrome .os-widgets__card-close:focus-visible {
@@ -1178,9 +1156,10 @@
1178 1156 * expose it (tapping it on a docked card would be a confusing
1179 1157 * no-op). The DOM node stays mounted either way so state flips
1180 1158 * don't rebuild chrome.
1181 1159 *
1182 - * Styling mirrors the inline close button: 20×20 round tile,
1160 + * Styling mirrors the inline close button: 24×24 round tile
1161 + * (the WCAG 2.2 SC 2.5.8 target-size floor),
1183 1162 * dimmed by default, full-opacity on hover / focus. Distinct hover
1184 1163 * color (theme accent, not red) signals "put back" vs. "remove."
1185 1164 */
1186 1165 .os-widgets__card-redock {
@@ -1191,10 +1170,10 @@
1191 1170 border: 0;
1192 1171 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
1193 1172 color: rgba( 255, 255, 255, 0.75 );
1194 1173 border-radius: 50%;
1195 - width: 20px;
1196 - height: 20px;
1174 + width: 24px;
1175 + height: 24px;
1197 1176 cursor: pointer;
1198 1177 opacity: 0.7;
1199 1178 transition:
1200 1179 opacity 0.15s ease,
@@ -1209,9 +1188,9 @@
1209 1188 .os-widgets__card-redock:hover,
1210 1189 .os-widgets__card-redock:focus-visible {
1211 1190 opacity: 1;
1212 1191 background: var( --wp-admin-theme-color, #2271b1 );
1213 - color: var( --os-ui-fg-on-accent, #fff );
1192 + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) );
1214 1193 }
1215 1194
1216 1195 /*
1217 1196 * Floating widgets — absolute-positioned, inline top/left/width/height
@@ -1221,8 +1200,18 @@
1221 1200 position: absolute;
1222 1201 margin: 0;
1223 1202 }
1224 1203
1204 +/*
1205 + * A liberated card is reparented to the desktop area, so the column's
1206 + * overview fade above no longer reaches it. Direct children only: a
1207 + * card inside a window body belongs to that window's thumbnail.
1208 + */
1209 +.os-area--overview > .os-widgets__card--floating {
1210 + opacity: 0;
1211 + pointer-events: none;
1212 +}
1213 +
1225 1214 .os-widgets__card--dragging,
1226 1215 .os-widgets__card--resizing {
1227 1216 transition: none;
1228 1217 z-index: 2;
@@ -1316,34 +1305,72 @@
1316 1305 display: none;
1317 1306 }
1318 1307
1319 1308 /*
1320 - * Add-widget tile — matches the "+" tile in the overview top bar.
1321 - * Always visible at the bottom of the column; promoted to center
1322 - * when the column is otherwise empty so first-run users see a
1323 - * friendly target rather than an empty strip.
1309 + * Add-widget tile — a compact pill that trails the widget stack, not
1310 + * a full-width drop zone. Hidden until the pointer comes near the
1311 + * column (the layer toggles `--hovered`), so the desktop stays clean
1312 + * for people who never touch widgets.
1313 + *
1314 + * Absolute rather than in flow because "the bottom of the stack" is
1315 + * not something the column's layout knows: a widget dragged out of
1316 + * the column becomes a floating card parented to the desktop, so it
1317 + * leaves the flex flow while still sitting visually in the column.
1318 + * The layer measures those too and writes `top` — see
1319 + * `positionAddTile()`.
1324 1320 */
1325 1321 .os-widgets__add {
1326 - margin-top: auto;
1327 - padding: 14px 12px;
1322 + position: absolute;
1323 + top: 0;
1324 + left: 50%;
1325 + transform: translateX( -50% );
1326 + padding: 8px 22px;
1328 1327 display: flex;
1329 1328 align-items: center;
1330 1329 justify-content: center;
1331 - gap: 8px;
1332 - pointer-events: auto;
1330 + gap: 7px;
1331 + pointer-events: none;
1333 1332 background: transparent;
1334 - border: 2px dashed rgba( 255, 255, 255, 0.22 );
1335 - border-radius: 14px;
1333 + border: 1px dashed rgba( 255, 255, 255, 0.22 );
1334 + border-radius: 10px;
1336 1335 color: rgba( 255, 255, 255, 0.72 );
1337 1336 font: inherit;
1338 1337 font-weight: 500;
1339 1338 cursor: pointer;
1339 + opacity: 0;
1340 1340 transition:
1341 + opacity 0.15s ease,
1341 1342 border-color 0.15s ease,
1342 1343 background-color 0.15s ease,
1343 1344 color 0.15s ease;
1344 1345 }
1345 1346
1347 +/*
1348 + * Reveal conditions. An empty column is no exception — approaching
1349 + * the right side is the gesture, whether or not there's a card there
1350 + * to aim at. `:focus-visible` keeps it reachable by keyboard, where
1351 + * there is no pointer to bring near.
1352 + */
1353 +.os-widgets--hovered .os-widgets__add,
1354 +.os-widgets--picking .os-widgets__add,
1355 +.os-widgets__add:focus-visible {
1356 + opacity: 1;
1357 + pointer-events: auto;
1358 +}
1359 +
1360 +/*
1361 + * Touch has no hover to approach the column with. The wallpaper's
1362 + * right-click menu carries an "Add widget" entry, but a long-press
1363 + * to reach it is a lot to ask of someone who just wants a clock, so
1364 + * on those devices the pill stays put.
1365 + */
1366 +@media ( hover: none ) {
1367 + .os-widgets__add {
1368 + opacity: 1;
1369 + pointer-events: auto;
1370 + }
1371 +}
1372 +
1346 1373 .os-widgets__add:hover {
1347 1374 border-color: rgba( 255, 255, 255, 0.4 );
1348 1375 background: rgba( 255, 255, 255, 0.06 );
1349 1376 color: var( --os-ui-fg-on-accent, #fff );
@@ -1354,11 +1381,11 @@
1354 1381 outline-offset: 2px;
1355 1382 }
1356 1383
1357 1384 .os-widgets__add-plus {
1358 - font-size: 20px;
1385 + font-size: 15px;
1359 1386 line-height: 1;
1360 - font-weight: 200;
1387 + font-weight: 300;
1361 1388 }
1362 1389
1363 1390 .os-widgets__add-label {
1364 1391 font-size: 13px;
@@ -1574,5 +1601,469 @@
1574 1601 os-notice a:hover,
1575 1602 os-notice a:focus,
1576 1603 os-notice a:active {
1577 1604 color: var(--os-ui-notice-link-hover, #fffbff);
1605 +}
1606 +
1607 +/* --------------------------------------------------------------------
1608 + * The workspace hop — cross-document view transitions between shells.
1609 + *
1610 + * Loaded only on the SHELL screen (this sheet never reaches chromeless
1611 + * iframes), so only shell→shell navigations transition: hopping between
1612 + * a site's desktop and the network admin's crossfades the two desktops
1613 + * instead of hard-cutting, which is what makes the per-admin sessions
1614 + * read as workspaces. Classic admin pages never opt in, so entering or
1615 + * leaving the shell stays a plain navigation, and reloads are excluded
1616 + * by the spec. Browsers without cross-document view transitions ignore
1617 + * all of this and navigate plainly. See docs/multisite.md.
1618 + */
1619 +@view-transition {
1620 + navigation: auto;
1621 +}
1622 +
1623 +/* Reduced motion swaps instantly — the hop still happens, the
1624 + * animation does not. */
1625 +@media ( prefers-reduced-motion: reduce ) {
1626 + @view-transition {
1627 + navigation: none;
1628 + }
1629 +}
1630 +
1631 +/* A gentle zoom-through: the desktop being left recedes as the one
1632 + * arriving settles in. Subtle on purpose — this runs on a full page
1633 + * swap, and anything louder reads as a reload effect. */
1634 +::view-transition-old(root) {
1635 + animation: os-workspace-hop-out 220ms cubic-bezier( 0.4, 0, 1, 1 ) both;
1636 +}
1637 +
1638 +::view-transition-new(root) {
1639 + animation: os-workspace-hop-in 280ms cubic-bezier( 0, 0, 0.2, 1 ) both;
1640 +}
1641 +
1642 +@keyframes os-workspace-hop-out {
1643 + to {
1644 + opacity: 0;
1645 + transform: scale( 0.985 );
1646 + }
1647 +}
1648 +
1649 +@keyframes os-workspace-hop-in {
1650 + from {
1651 + opacity: 0;
1652 + transform: scale( 1.015 );
1653 + }
1654 +}
1655 +
1656 +/* The instance hop's two halves, on either side of that page swap.
1657 + * The switcher slides this desk out towards the site it picked
1658 + * (`os-shell--hop-out-next` / `-prev`, for the beat before it
1659 + * navigates), and a shell asked to boot into overview arrives with its
1660 + * desk hidden (`os-shell--arriving`, stamped server-side) until overview
1661 + * is up, then slides it in from the same side (`-next` / `-prev`, the
1662 + * hint the switcher left in sessionStorage; no hint, a plain fade). The
1663 + * wallpaper never moves, which is what makes it read as the tiles
1664 + * changing rather than the page. `src/multisite/instance-transition.ts`
1665 + * drives the classes. */
1666 +.os-shell--hop-out-next #os-area,
1667 +.os-shell--hop-out-prev #os-area {
1668 + opacity: 0;
1669 + transition:
1670 + opacity 220ms cubic-bezier( 0.4, 0, 1, 1 ),
1671 + transform 220ms cubic-bezier( 0.4, 0, 1, 1 );
1672 +}
1673 +
1674 +.os-shell--hop-out-next #os-area {
1675 + transform: translateX( -48px );
1676 +}
1677 +
1678 +.os-shell--hop-out-prev #os-area {
1679 + transform: translateX( 48px );
1680 +}
1681 +
1682 +.os-shell--hop-out-next .os-dock,
1683 +.os-shell--hop-out-prev .os-dock,
1684 +.os-shell--arriving .os-dock {
1685 + opacity: 0;
1686 + transition: opacity 220ms ease;
1687 +}
1688 +
1689 +.os-shell--arriving #os-area {
1690 + opacity: 0;
1691 + transition: none;
1692 +}
1693 +
1694 +.os-shell--arriving-next #os-area {
1695 + transform: translateX( 48px );
1696 +}
1697 +
1698 +.os-shell--arriving-prev #os-area {
1699 + transform: translateX( -48px );
1700 +}
1701 +
1702 +/* The reveal: the arriving classes come off and this goes on in the
1703 + * same frame, so the desk transitions from hidden to its resting
1704 + * place. */
1705 +.os-shell--revealing #os-area {
1706 + transition:
1707 + opacity 360ms cubic-bezier( 0, 0, 0.2, 1 ),
1708 + transform 360ms cubic-bezier( 0, 0, 0.2, 1 );
1709 +}
1710 +
1711 +.os-shell--revealing .os-dock {
1712 + transition: opacity 360ms ease;
1713 +}
1714 +
1715 +/* A desk must never stay hidden: if the boot that reveals it never
1716 + * runs, the area and the dock come back on their own a few seconds
1717 + * in. */
1718 +@keyframes os-instance-arrive-fallback {
1719 + to {
1720 + opacity: 1;
1721 + transform: none;
1722 + }
1723 +}
1724 +
1725 +.os-shell--arriving #os-area,
1726 +.os-shell--arriving .os-dock {
1727 + animation: os-instance-arrive-fallback 1ms linear 6s forwards;
1728 +}
1729 +
1730 +@media ( prefers-reduced-motion: reduce ) {
1731 + .os-shell--hop-out-next #os-area,
1732 + .os-shell--hop-out-prev #os-area,
1733 + .os-shell--arriving-next #os-area,
1734 + .os-shell--arriving-prev #os-area {
1735 + transform: none;
1736 + }
1737 +
1738 + .os-shell--hop-out-next #os-area,
1739 + .os-shell--hop-out-prev #os-area,
1740 + .os-shell--hop-out-next .os-dock,
1741 + .os-shell--hop-out-prev .os-dock,
1742 + .os-shell--revealing #os-area,
1743 + .os-shell--revealing .os-dock {
1744 + transition: none;
1745 + }
1746 +}
1747 +
1748 +/* A residency frame follows the window body without scrolling with its contents. */
1749 +.os-mio-residence {
1750 + position: absolute;
1751 + z-index: 50;
1752 + pointer-events: none;
1753 + overflow: hidden;
1754 + container-type: inline-size;
1755 +}
1756 +
1757 +.os-mio-residence[hidden],
1758 +.os-mio-chat-launcher[hidden] {
1759 + display: none;
1760 +}
1761 +
1762 +.os-mio-chat-launcher {
1763 + position: absolute;
1764 + inset-inline-end: 20px;
1765 + inset-block-end: 18px;
1766 + pointer-events: auto;
1767 +}
1768 +
1769 +.os-mio-chat {
1770 + position: absolute;
1771 + inset-inline-end: 16px;
1772 + inset-block-end: 16px;
1773 + inline-size: min(380px, calc(100% - 32px));
1774 + max-block-size: calc(100% - 32px);
1775 + box-sizing: border-box;
1776 + display: flex;
1777 + flex-direction: column;
1778 + gap: 12px;
1779 + padding: 18px;
1780 + pointer-events: auto;
1781 + color: var(--os-mio-chat-fg, #1d2327);
1782 + background: var(--os-mio-chat-bg, #fff);
1783 + border: 1px solid var(--os-mio-chat-border, #c3c4c7);
1784 + border-radius: var(--os-mio-chat-radius, 16px);
1785 + box-shadow: 0 8px 40px var(--os-mio-chat-glow, #00000020);
1786 + animation: os-mio-chat-enter 180ms ease-out;
1787 +}
1788 +
1789 +.os-mio-chat header {
1790 + display: flex;
1791 + align-items: center;
1792 + gap: 12px;
1793 +}
1794 +
1795 +.os-mio-chat header strong {
1796 + letter-spacing: .14em;
1797 +}
1798 +
1799 +.os-mio-chat header span {
1800 + flex: 1;
1801 + font-size: 11px;
1802 + color: var(--os-mio-chat-muted, #646970);
1803 +}
1804 +
1805 +.os-mio-chat__log {
1806 + position: relative;
1807 + min-block-size: 60px;
1808 + max-block-size: 300px;
1809 + overflow: auto;
1810 + overscroll-behavior: contain;
1811 +}
1812 +
1813 +.os-mio-chat__message {
1814 + margin-block: 0 10px;
1815 + padding: 10px 12px;
1816 + line-height: 1.6;
1817 + white-space: pre-wrap;
1818 + overflow-wrap: anywhere;
1819 +}
1820 +
1821 +.os-mio-chat__message--user {
1822 + border-radius: 16px 16px 4px 16px;
1823 + background: var(--os-mio-chat-user-bg, #f0f0f1);
1824 +}
1825 +
1826 +.os-mio-chat__composer {
1827 + display: flex;
1828 + align-items: center;
1829 + gap: 8px;
1830 +}
1831 +
1832 +.os-mio-chat__composer os-textarea {
1833 + flex: 1;
1834 + min-inline-size: 0;
1835 +}
1836 +
1837 +.os-mio-chat__status,
1838 +.os-mio-chat small {
1839 + margin: 0;
1840 + font-size: 11px;
1841 + line-height: 1.5;
1842 + color: var(--os-mio-chat-muted, #646970);
1843 +}
1844 +
1845 +@keyframes os-mio-chat-enter {
1846 + from { opacity: 0; transform: translateY(6px) scale(.98); }
1847 + to { opacity: 1; transform: translateY(0) scale(1); }
1848 +}
1849 +
1850 +@media (prefers-reduced-motion: reduce) {
1851 + .os-mio-chat { animation: none; }
1852 +}
1853 +
1854 +.os-mio-residence > .os-mio { z-index: 0; }
1855 +.os-mio-chat, .os-mio-chat-launcher { z-index: 1; }
1856 +
1857 +.os-mio-chat [hidden] {
1858 + display: none !important;
1859 +}
1860 +
1861 +.os-mio-chat__message--assistant {
1862 + white-space: normal;
1863 +}
1864 +
1865 +.os-mio-chat__message p {
1866 + margin-block: 0 8px;
1867 +}
1868 +
1869 +.os-mio-chat__message ul,
1870 +.os-mio-chat__message ol {
1871 + margin-block: 8px;
1872 + padding-inline-start: 20px;
1873 +}
1874 +
1875 +.os-mio-chat__message a {
1876 + color: inherit;
1877 + text-decoration: underline;
1878 +}
1879 +
1880 +/* Shared MIO chrome: the same portrait with a softly revealed off-state slash. */
1881 +.os-mio-window-toggle svg {
1882 + width: 20px;
1883 + height: 20px;
1884 + overflow: visible;
1885 +}
1886 +
1887 +.os-mio-window-toggle__slash {
1888 + fill: none;
1889 + stroke: currentColor;
1890 + stroke-width: 2.4;
1891 + stroke-linecap: round;
1892 + opacity: 0;
1893 + stroke-dasharray: 26;
1894 + stroke-dashoffset: 26;
1895 + transition: opacity 240ms ease, stroke-dashoffset 240ms ease;
1896 +}
1897 +
1898 +.os-mio-window-toggle[data-mio-disabled="true"] .os-mio-window-toggle__slash {
1899 + opacity: 1;
1900 + stroke-dashoffset: 0;
1901 +}
1902 +
1903 +.os-mio-chat__activity {
1904 + display: flex;
1905 + align-items: center;
1906 + gap: 8px;
1907 + min-height: 18px;
1908 +}
1909 +
1910 +.os-mio-chat__thinking {
1911 + display: inline-flex;
1912 + align-items: center;
1913 + gap: 3px;
1914 + width: 24px;
1915 + flex-shrink: 0;
1916 + opacity: 0;
1917 + color: var(--os-mio-chat-muted, #646970);
1918 + transition: opacity 240ms ease;
1919 +}
1920 +
1921 +.os-mio-chat[data-thinking="true"] .os-mio-chat__thinking {
1922 + opacity: 1;
1923 +}
1924 +
1925 +.os-mio-chat__thinking i {
1926 + width: 4px;
1927 + height: 4px;
1928 + border-radius: 50%;
1929 + background: currentColor;
1930 + animation: os-mio-thinking 1.6s ease-in-out infinite;
1931 + animation-play-state: paused;
1932 +}
1933 +
1934 +.os-mio-chat__thinking i:nth-child(2) { animation-delay: 160ms; }
1935 +.os-mio-chat__thinking i:nth-child(3) { animation-delay: 320ms; }
1936 +.os-mio-chat[data-thinking="true"] .os-mio-chat__thinking i { animation-play-state: running; }
1937 +.os-mio-window-toggle[data-mio-thinking="true"] circle { animation: os-mio-thinking 2.4s ease-in-out infinite; }
1938 +
1939 +@keyframes os-mio-thinking {
1940 + 0%, 100% { opacity: 0.45; }
1941 + 50% { opacity: 1; }
1942 +}
1943 +
1944 +@media (prefers-reduced-motion: reduce) {
1945 + .os-mio-window-toggle__slash,
1946 + .os-mio-chat__thinking { transition: none; }
1947 + .os-mio-chat__thinking i,
1948 + .os-mio-window-toggle[data-mio-thinking="true"] circle { animation: none; }
1949 +}
1950 +
1951 +.os-mio-window-toggle {
1952 + max-inline-size: 30px;
1953 + opacity: 1;
1954 + visibility: visible;
1955 + transition: opacity 180ms ease, max-inline-size 240ms ease, visibility 0s;
1956 +}
1957 +
1958 +.os-mio-window-toggle[data-mio-available="false"] {
1959 + max-inline-size: 0;
1960 + opacity: 0;
1961 + visibility: hidden;
1962 + overflow: hidden;
1963 + pointer-events: none;
1964 + transition: opacity 180ms ease, max-inline-size 240ms ease, visibility 0s 180ms;
1965 +}
1966 +
1967 +@media (prefers-reduced-motion: reduce) {
1968 + .os-mio-window-toggle,
1969 + .os-mio-window-toggle[data-mio-available="false"] { transition: none; }
1970 +}
1971 +
1972 +/* The launcher stays available; the companion only speaks when invited. */
1973 +.os-mio > canvas,
1974 +.os-mio > .os-mio__handle {
1975 + transition: opacity 180ms ease, visibility 180ms;
1976 +}
1977 +
1978 +.os-mio[data-mio-visible="false"] > canvas,
1979 +.os-mio[data-mio-visible="false"] > .os-mio__handle {
1980 + opacity: 0;
1981 + visibility: hidden;
1982 + pointer-events: none;
1983 +}
1984 +
1985 +.os-mio-callout {
1986 + position: absolute;
1987 + z-index: 2;
1988 + display: flex;
1989 + align-items: center;
1990 + gap: 10px;
1991 + max-inline-size: calc(100% - 16px);
1992 + box-sizing: border-box;
1993 + padding-block: 6px;
1994 + padding-inline: 15px 6px;
1995 + border: 1px solid var(--os-mio-callout-border, #d6b8cb);
1996 + border-radius: 22px;
1997 + background: var(--os-mio-callout-bg, #ffe0cf);
1998 + color: var(--os-mio-callout-fg, #382b38);
1999 + box-shadow: 0 4px 16px var(--os-mio-chat-glow, #00000020);
2000 + font-size: 12px;
2001 + line-height: 1.5;
2002 + font-weight: 500;
2003 + pointer-events: auto;
2004 + animation: os-mio-chat-enter 180ms ease-out;
2005 +}
2006 +
2007 +.os-mio-callout os-button::part(button) {
2008 + inline-size: 26px;
2009 + block-size: 26px;
2010 + font-size: 17px;
2011 + line-height: 1;
2012 +}
2013 +
2014 +/* A small rounded tail joins the companion without a second outlined shape. */
2015 +.os-mio-callout::after {
2016 + content: "";
2017 + position: absolute;
2018 + inset-block-end: -4px;
2019 + inset-inline-end: 18px;
2020 + inline-size: 8px;
2021 + block-size: 8px;
2022 + background: inherit;
2023 + border-inline-end: inherit;
2024 + border-block-end: inherit;
2025 + border-end-end-radius: 3px;
2026 + transform: rotate(45deg);
2027 +}
2028 +
2029 +.os-mio-callout[hidden] { display: none; }
2030 +
2031 +@media (prefers-reduced-motion: reduce) {
2032 + .os-mio > canvas,
2033 + .os-mio > .os-mio__handle { transition: none; }
2034 + .os-mio-callout { animation: none; }
2035 +}
2036 +
2037 +/* Response controls belong to their message; all layout follows inline flow. */
2038 +.os-mio-chat__actions {
2039 + display: flex;
2040 + flex-wrap: wrap;
2041 + gap: 8px;
2042 + margin-block-start: 12px;
2043 +}
2044 +
2045 +.os-mio-chat__action {
2046 + display: flex;
2047 + flex-direction: column;
2048 + align-items: flex-start;
2049 + gap: 4px;
2050 + min-inline-size: 0;
2051 + max-inline-size: 100%;
2052 +}
2053 +
2054 +.os-mio-chat__action os-button::part(button) {
2055 + white-space: normal;
2056 + overflow-wrap: anywhere;
2057 +}
2058 +
2059 +.os-mio-chat__action-status {
2060 + font-size: 12px;
2061 + color: var(--os-mio-chat-muted, #646970);
2062 + overflow-wrap: anywhere;
2063 +}
2064 +
2065 +.os-mio-chat__composer os-textarea::part(textarea) {
2066 + white-space: pre-wrap;
2067 + overflow-wrap: anywhere;
2068 + overflow-x: hidden;
1578 2069 }