@apliteni/apliteni-ui 0.128.0 → 0.130.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/react/README.md +7 -0
- package/react/dist/index.css +48 -46
- package/react/dist/index.d.ts +3 -0
- package/react/dist/index.js +62 -71
- package/src/assets/icons.js +5 -0
- package/src/components/drawer.js +5 -2
- package/src/components/overlay.js +9 -5
- package/src/components/shell.js +75 -9
- package/src/components/toasts.js +38 -14
- package/src/styles/callout.css +13 -3
- package/src/styles/layout.css +50 -78
- package/src/styles/topbar.css +9 -0
package/package.json
CHANGED
package/react/README.md
CHANGED
|
@@ -585,6 +585,13 @@ or neutral. Notices disappear after five seconds, pausing while hovered or focus
|
|
|
585
585
|
Danger notices are announced assertively; other tones are polite. An `action` with a `label` and
|
|
586
586
|
`onClick` stays until selected or dismissed. New notices appear at the bottom.
|
|
587
587
|
|
|
588
|
+
Pass `compact: true` to show only the title, or `dismissible: false` to hide the
|
|
589
|
+
close button. Hiding the close button keeps the five-second timer for notices
|
|
590
|
+
without actions; notices with actions remain until their action is selected, so a
|
|
591
|
+
notice with an action and no close button has no other dismissal affordance.
|
|
592
|
+
Pass `timer: 12` to dismiss a notice after that many seconds. Pass `timer: false` or `timer: 0`
|
|
593
|
+
to keep a notice until it is dismissed.
|
|
594
|
+
|
|
588
595
|
```tsx
|
|
589
596
|
function SaveButton() {
|
|
590
597
|
const push = useToast();
|
package/react/dist/index.css
CHANGED
|
@@ -852,7 +852,7 @@
|
|
|
852
852
|
.rx-toast-stack {
|
|
853
853
|
position: fixed;
|
|
854
854
|
right: var(--space-4);
|
|
855
|
-
bottom:
|
|
855
|
+
bottom: var(--space-4);
|
|
856
856
|
max-width: calc(100% - 2 * var(--space-4));
|
|
857
857
|
z-index: var(--z-toast);
|
|
858
858
|
pointer-events: none;
|
|
@@ -869,9 +869,6 @@
|
|
|
869
869
|
}
|
|
870
870
|
|
|
871
871
|
/* src/AppShell.css */
|
|
872
|
-
.ui-react-app__bottom {
|
|
873
|
-
display: none;
|
|
874
|
-
}
|
|
875
872
|
.ui-react-app__actions {
|
|
876
873
|
display: flex;
|
|
877
874
|
flex-wrap: wrap;
|
|
@@ -882,66 +879,71 @@
|
|
|
882
879
|
display: inline-flex;
|
|
883
880
|
opacity: 1;
|
|
884
881
|
}
|
|
885
|
-
.ui-react-app__bottom .ui-nav__item {
|
|
886
|
-
min-height: 48px;
|
|
887
|
-
}
|
|
888
882
|
.ui-react-app:not(:has(> .ui-app__rail)) {
|
|
889
883
|
grid-template-columns: minmax(0, 1fr);
|
|
890
884
|
}
|
|
891
|
-
.ui-react-app .ui-app__bar > .ui-app__brand {
|
|
885
|
+
.ui-react-app .ui-app__bar > .ui-app__brand:not(.ui-app__brand--band) {
|
|
892
886
|
flex: none;
|
|
893
887
|
margin-inline-end: var(--space-2);
|
|
894
888
|
}
|
|
889
|
+
.ui-react-app__menu .ui-nav--side,
|
|
890
|
+
.ui-react-app__menu .ui-nav--side > * {
|
|
891
|
+
width: 100%;
|
|
892
|
+
}
|
|
893
|
+
.ui-react-app__menu .ui-nav__item {
|
|
894
|
+
min-height: 44px;
|
|
895
|
+
}
|
|
896
|
+
:where(:root[data-theme=light]) .ui-react-app__menu .ui-nav__item:not(.is-danger):not(.is-active):hover {
|
|
897
|
+
background: color-mix(in srgb, var(--surface-3) 50%, transparent);
|
|
898
|
+
}
|
|
899
|
+
:where(:root[data-theme=light]) .ui-react-app__menu .ui-nav__item.is-active {
|
|
900
|
+
background: var(--surface-3);
|
|
901
|
+
}
|
|
902
|
+
.ui-react-app__band-control {
|
|
903
|
+
display: contents;
|
|
904
|
+
}
|
|
895
905
|
@media (max-width: 560px) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
:root:has(.ui-react-app) {
|
|
900
|
-
scroll-padding-bottom: calc(var(--space-5) + var(--ui-app-bottom-clearance, 0px));
|
|
901
|
-
}
|
|
902
|
-
.ui-react-app {
|
|
903
|
-
display: block;
|
|
906
|
+
.ui-react-app .ui-app__bar {
|
|
907
|
+
gap: var(--space-1);
|
|
908
|
+
padding-inline: var(--space-2);
|
|
904
909
|
}
|
|
905
|
-
.ui-react-app
|
|
910
|
+
.ui-react-app .ui-app__bar-gap {
|
|
906
911
|
display: none;
|
|
907
912
|
}
|
|
908
|
-
|
|
909
|
-
padding-bottom: var(--ui-app-bottom-clearance, 0px);
|
|
910
|
-
}
|
|
911
|
-
.ui-react-app .ui-app__bar {
|
|
912
|
-
padding-inline: var(--space-4);
|
|
913
|
-
}
|
|
914
|
-
.ui-react-app .ui-app__bar > .ui-app__brand {
|
|
913
|
+
.ui-react-app .ui-app__search-ic {
|
|
915
914
|
display: none;
|
|
916
915
|
}
|
|
917
|
-
.ui-react-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
background: var(--bg);
|
|
924
|
-
border-top: 1px solid var(--border);
|
|
925
|
-
}
|
|
926
|
-
.ui-react-app__bottom .ui-nav__list {
|
|
916
|
+
.ui-react-app .ui-app__search {
|
|
917
|
+
flex: 1 1 auto;
|
|
918
|
+
min-width: 56px;
|
|
919
|
+
padding-inline: var(--space-2);
|
|
920
|
+
}
|
|
921
|
+
.ui-react-app__band-control {
|
|
927
922
|
display: flex;
|
|
928
|
-
flex
|
|
923
|
+
flex: 0 1 auto;
|
|
924
|
+
min-width: 56px;
|
|
925
|
+
max-width: min(90px, calc(100vw - 261px));
|
|
929
926
|
}
|
|
930
|
-
.ui-react-
|
|
931
|
-
flex: 1;
|
|
927
|
+
.ui-react-app__band-control > .ui-btn {
|
|
932
928
|
min-width: 0;
|
|
929
|
+
min-height: 34px;
|
|
930
|
+
max-width: 100%;
|
|
933
931
|
}
|
|
934
|
-
.ui-react-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
932
|
+
.ui-react-app .ui-app__bar .toggle {
|
|
933
|
+
margin-inline-start: 1px;
|
|
934
|
+
}
|
|
935
|
+
.ui-react-app .ui-app__user--bar {
|
|
936
|
+
margin-inline-start: 2px;
|
|
937
|
+
}
|
|
938
|
+
.ui-react-app .ui-app__user-trigger {
|
|
939
|
+
min-width: 44px;
|
|
939
940
|
}
|
|
940
|
-
.ui-react-
|
|
941
|
-
|
|
942
|
-
|
|
941
|
+
.ui-react-app__band-control > .ui-btn .ui-btn__label {
|
|
942
|
+
overflow: hidden;
|
|
943
|
+
text-overflow: ellipsis;
|
|
944
|
+
white-space: nowrap;
|
|
943
945
|
}
|
|
944
|
-
.ui-react-
|
|
946
|
+
.ui-react-app .ui-app__bar > .ui-app__brand:not(.ui-app__brand--band) {
|
|
945
947
|
display: none;
|
|
946
948
|
}
|
|
947
949
|
}
|
package/react/dist/index.d.ts
CHANGED
|
@@ -778,6 +778,9 @@ declare const SearchField: react.ForwardRefExoticComponent<Omit<InputHTMLAttribu
|
|
|
778
778
|
|
|
779
779
|
type ToastNotice = {
|
|
780
780
|
tone?: 'success' | 'danger' | 'warn' | 'info' | 'neutral';
|
|
781
|
+
compact?: boolean;
|
|
782
|
+
dismissible?: boolean;
|
|
783
|
+
timer?: boolean | number;
|
|
781
784
|
title: string;
|
|
782
785
|
text?: string;
|
|
783
786
|
action?: {
|
package/react/dist/index.js
CHANGED
|
@@ -3432,11 +3432,13 @@ function Notice({ notice, remove }) {
|
|
|
3432
3432
|
const root = useRef18(null);
|
|
3433
3433
|
const [leaving2, setLeaving] = useState12(false);
|
|
3434
3434
|
const dismissed = useRef18(false);
|
|
3435
|
-
const remaining = useRef18(5e3);
|
|
3436
3435
|
const [hovered, setHovered] = useState12(false);
|
|
3437
3436
|
const [focused, setFocused] = useState12(false);
|
|
3438
3437
|
const paused = hovered || focused;
|
|
3439
3438
|
const tone = notice.tone ?? "success";
|
|
3439
|
+
const timed = !notice.action && notice.timer !== false && notice.timer !== 0;
|
|
3440
|
+
const duration = typeof notice.timer === "number" ? notice.timer * 1e3 : 5e3;
|
|
3441
|
+
const remaining = useRef18(duration);
|
|
3440
3442
|
const dismiss = useCallback5(() => {
|
|
3441
3443
|
if (dismissed.current) return;
|
|
3442
3444
|
dismissed.current = true;
|
|
@@ -3458,19 +3460,19 @@ function Notice({ notice, remove }) {
|
|
|
3458
3460
|
}
|
|
3459
3461
|
}, [leaving2, remove]);
|
|
3460
3462
|
useEffect12(() => {
|
|
3461
|
-
if (leaving2 ||
|
|
3463
|
+
if (leaving2 || !timed || paused) return;
|
|
3462
3464
|
const started = Date.now();
|
|
3463
3465
|
const timer = setTimeout(dismiss, remaining.current);
|
|
3464
3466
|
return () => {
|
|
3465
3467
|
clearTimeout(timer);
|
|
3466
3468
|
remaining.current = Math.max(0, remaining.current - (Date.now() - started));
|
|
3467
3469
|
};
|
|
3468
|
-
}, [dismiss, leaving2,
|
|
3470
|
+
}, [dismiss, leaving2, paused, timed]);
|
|
3469
3471
|
return /* @__PURE__ */ jsxs27(
|
|
3470
3472
|
"div",
|
|
3471
3473
|
{
|
|
3472
3474
|
ref: root,
|
|
3473
|
-
className: `ui-toast ui-toast--${tone} ui-toast--soft${leaving2 ? " is-leaving" : ""}`,
|
|
3475
|
+
className: `ui-toast ui-toast--${tone} ui-toast--soft${notice.compact ? " ui-toast--compact" : ""}${leaving2 ? " is-leaving" : ""}`,
|
|
3474
3476
|
role: tone === "danger" ? "alert" : "status",
|
|
3475
3477
|
"aria-live": tone === "danger" ? "assertive" : "polite",
|
|
3476
3478
|
onMouseEnter: () => setHovered(true),
|
|
@@ -3483,7 +3485,7 @@ function Notice({ notice, remove }) {
|
|
|
3483
3485
|
/* @__PURE__ */ jsx32("span", { className: "ui-toast__icon", children: /* @__PURE__ */ jsx32(Icon, { name: glyphs[tone] }) }),
|
|
3484
3486
|
/* @__PURE__ */ jsxs27("div", { className: "ui-toast__body", children: [
|
|
3485
3487
|
/* @__PURE__ */ jsx32("div", { className: "ui-toast__title", children: notice.title }),
|
|
3486
|
-
notice.text && /* @__PURE__ */ jsx32("div", { className: "ui-toast__text", children: notice.text })
|
|
3488
|
+
!notice.compact && notice.text && /* @__PURE__ */ jsx32("div", { className: "ui-toast__text", children: notice.text })
|
|
3487
3489
|
] }),
|
|
3488
3490
|
notice.action && /* @__PURE__ */ jsx32(
|
|
3489
3491
|
"button",
|
|
@@ -3500,13 +3502,13 @@ function Notice({ notice, remove }) {
|
|
|
3500
3502
|
children: notice.action.label
|
|
3501
3503
|
}
|
|
3502
3504
|
),
|
|
3503
|
-
/* @__PURE__ */ jsx32("button", { type: "button", className: "ui-toast__close", "aria-label": "Dismiss", disabled: leaving2, onClick: dismiss, children: /* @__PURE__ */ jsx32(Icon, { name: "x" }) }),
|
|
3504
|
-
|
|
3505
|
+
notice.dismissible !== false && /* @__PURE__ */ jsx32("button", { type: "button", className: "ui-toast__close", "aria-label": "Dismiss", disabled: leaving2, onClick: dismiss, children: /* @__PURE__ */ jsx32(Icon, { name: "x" }) }),
|
|
3506
|
+
timed && /* @__PURE__ */ jsx32(
|
|
3505
3507
|
"span",
|
|
3506
3508
|
{
|
|
3507
3509
|
className: "ui-toast__timer is-running",
|
|
3508
3510
|
"aria-hidden": "true",
|
|
3509
|
-
style: { animationPlayState: paused ? "paused" : "running" }
|
|
3511
|
+
style: { animationDuration: `${duration}ms`, animationPlayState: paused ? "paused" : "running" }
|
|
3510
3512
|
}
|
|
3511
3513
|
)
|
|
3512
3514
|
]
|
|
@@ -3553,7 +3555,7 @@ function Toast({ children }) {
|
|
|
3553
3555
|
}
|
|
3554
3556
|
|
|
3555
3557
|
// src/AppShell.tsx
|
|
3556
|
-
import { useEffect as useEffect14, useId as useId12, useState as useState14 } from "react";
|
|
3558
|
+
import { useEffect as useEffect14, useId as useId12, useRef as useRef19, useState as useState14 } from "react";
|
|
3557
3559
|
import { paletteHotkey, prism, railCollapsed } from "@apliteni/apliteni-ui";
|
|
3558
3560
|
|
|
3559
3561
|
// src/SidebarNav.tsx
|
|
@@ -3751,16 +3753,26 @@ function AppShell({
|
|
|
3751
3753
|
const uid = useId12();
|
|
3752
3754
|
const [collapsed, setCollapsed] = useState14(defaultCollapsed);
|
|
3753
3755
|
const [search, setSearch] = useState14(false);
|
|
3754
|
-
const [
|
|
3756
|
+
const [menu, setMenu] = useState14(false);
|
|
3757
|
+
const menuButton = useRef19(null);
|
|
3755
3758
|
const current = path(pathname);
|
|
3756
3759
|
const active = sections.filter((section) => current === path(section.href) || path(section.href) !== "/" && current.startsWith(`${path(section.href)}/`)).sort((a, b) => path(b.href).length - path(a.href).length)[0];
|
|
3757
3760
|
useEffect14(() => {
|
|
3758
3761
|
setCollapsed(railCollapsed() ?? defaultCollapsed);
|
|
3759
3762
|
}, [defaultCollapsed]);
|
|
3760
3763
|
useEffect14(() => {
|
|
3761
|
-
|
|
3764
|
+
setMenu(false);
|
|
3762
3765
|
setSearch(false);
|
|
3763
3766
|
}, [pathname]);
|
|
3767
|
+
useEffect14(() => {
|
|
3768
|
+
if (!menu) return;
|
|
3769
|
+
const grown = () => {
|
|
3770
|
+
const button = menuButton.current;
|
|
3771
|
+
if (button && getComputedStyle(button).display === "none") setMenu(false);
|
|
3772
|
+
};
|
|
3773
|
+
window.addEventListener("resize", grown);
|
|
3774
|
+
return () => window.removeEventListener("resize", grown);
|
|
3775
|
+
}, [menu]);
|
|
3764
3776
|
useEffect14(() => {
|
|
3765
3777
|
const hotkey = (event) => {
|
|
3766
3778
|
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "k") return;
|
|
@@ -3780,36 +3792,17 @@ function AppShell({
|
|
|
3780
3792
|
} catch {
|
|
3781
3793
|
}
|
|
3782
3794
|
};
|
|
3783
|
-
const
|
|
3784
|
-
const linkProps = {
|
|
3785
|
-
href: section.href,
|
|
3786
|
-
className: `ui-nav__item${section === active ? " is-active" : ""}`,
|
|
3787
|
-
// guidelines/going-back.md, section-lit: the same row, so the same reading as
|
|
3788
|
-
// the rail's. "page" here would announce the list as the page the reader is on.
|
|
3789
|
-
"aria-current": section === active ? back ? "true" : "page" : void 0,
|
|
3790
|
-
"aria-label": `${section.label}${section.count == null ? "" : ` ${section.count}`}`,
|
|
3791
|
-
onClick: (event) => {
|
|
3792
|
-
if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
|
|
3793
|
-
},
|
|
3794
|
-
children: /* @__PURE__ */ jsxs30(Fragment12, { children: [
|
|
3795
|
-
/* @__PURE__ */ jsx35("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx35(Icon, { name: section.icon }) }),
|
|
3796
|
-
/* @__PURE__ */ jsx35("span", { className: "ui-nav__label", children: section.label }),
|
|
3797
|
-
section.count != null && /* @__PURE__ */ jsx35("span", { className: "ui-nav__badge is-neutral", children: section.count })
|
|
3798
|
-
] })
|
|
3799
|
-
};
|
|
3800
|
-
return /* @__PURE__ */ jsx35("li", { children: renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx35("a", { ...linkProps }) }, section.href);
|
|
3801
|
-
});
|
|
3802
|
-
const sidebar = (items, label) => /* @__PURE__ */ jsx35(
|
|
3795
|
+
const sidebar = (label) => /* @__PURE__ */ jsx35(
|
|
3803
3796
|
SidebarNav,
|
|
3804
3797
|
{
|
|
3805
3798
|
"aria-label": label,
|
|
3806
|
-
items:
|
|
3799
|
+
items: sections.map((section) => ({ ...section, id: section.href, badge: section.count })),
|
|
3807
3800
|
active: active?.href,
|
|
3808
3801
|
activeIs: back ? "section" : "page",
|
|
3809
3802
|
renderLink: (item, props) => {
|
|
3810
|
-
const section =
|
|
3803
|
+
const section = sections.find((section2) => section2.href === item.id);
|
|
3811
3804
|
const linkProps = { ...props, onClick: (event) => {
|
|
3812
|
-
if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey)
|
|
3805
|
+
if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMenu(false);
|
|
3813
3806
|
} };
|
|
3814
3807
|
return renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx35("a", { ...linkProps });
|
|
3815
3808
|
}
|
|
@@ -3817,8 +3810,6 @@ function AppShell({
|
|
|
3817
3810
|
);
|
|
3818
3811
|
const foldLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
|
|
3819
3812
|
const navigable = sections.length > 1 || Boolean(back);
|
|
3820
|
-
const visible = sections.length > 4 ? sections.slice(0, 3) : sections;
|
|
3821
|
-
const remaining = sections.slice(visible.length);
|
|
3822
3813
|
const initials = account.name.trim().split(/\s+/).map((part) => part[0]).slice(0, 2).join("");
|
|
3823
3814
|
const lockup = /* @__PURE__ */ jsxs30("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
|
|
3824
3815
|
brand ?? /* @__PURE__ */ jsx35("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
|
|
@@ -3827,7 +3818,7 @@ function AppShell({
|
|
|
3827
3818
|
return /* @__PURE__ */ jsxs30("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
|
|
3828
3819
|
navigable && /* @__PURE__ */ jsxs30("div", { className: "ui-app__rail", role: "region", "aria-label": "Sidebar", children: [
|
|
3829
3820
|
/* @__PURE__ */ jsx35("div", { className: "ui-app__head", children: lockup }),
|
|
3830
|
-
sidebar(
|
|
3821
|
+
sidebar("Sections"),
|
|
3831
3822
|
/* @__PURE__ */ jsx35("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx35("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs30("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
|
|
3832
3823
|
/* @__PURE__ */ jsx35("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs30("svg", { className: "ui-icon", viewBox: "0 0 24 24", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
3833
3824
|
/* @__PURE__ */ jsx35("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
|
|
@@ -3839,6 +3830,23 @@ function AppShell({
|
|
|
3839
3830
|
] }),
|
|
3840
3831
|
/* @__PURE__ */ jsxs30("div", { className: "ui-app__well", children: [
|
|
3841
3832
|
/* @__PURE__ */ jsxs30("header", { className: "ui-app__bar", children: [
|
|
3833
|
+
navigable && /* @__PURE__ */ jsxs30(Fragment12, { children: [
|
|
3834
|
+
/* @__PURE__ */ jsx35(
|
|
3835
|
+
Button,
|
|
3836
|
+
{
|
|
3837
|
+
ref: menuButton,
|
|
3838
|
+
variant: "ghost",
|
|
3839
|
+
icon: "menu",
|
|
3840
|
+
iconOnly: true,
|
|
3841
|
+
"aria-label": "Menu",
|
|
3842
|
+
className: "ui-app__menu",
|
|
3843
|
+
"aria-haspopup": "dialog",
|
|
3844
|
+
"aria-expanded": menu,
|
|
3845
|
+
onClick: () => setMenu(true)
|
|
3846
|
+
}
|
|
3847
|
+
),
|
|
3848
|
+
/* @__PURE__ */ jsx35("a", { className: "ui-app__brand ui-app__brand--band", href: brandHref, "aria-label": word, children: brand ?? /* @__PURE__ */ jsx35("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(`${uid}-band`, 24) } }) })
|
|
3849
|
+
] }),
|
|
3842
3850
|
!navigable && lockup,
|
|
3843
3851
|
/* @__PURE__ */ jsxs30("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
|
|
3844
3852
|
/* @__PURE__ */ jsx35("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx35(Icon, { name: "search" }) }),
|
|
@@ -3847,7 +3855,7 @@ function AppShell({
|
|
|
3847
3855
|
/* @__PURE__ */ jsx35("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
|
|
3848
3856
|
] }),
|
|
3849
3857
|
/* @__PURE__ */ jsx35("span", { className: "ui-app__bar-gap" }),
|
|
3850
|
-
bandControl,
|
|
3858
|
+
bandControl && /* @__PURE__ */ jsx35("span", { className: "ui-react-app__band-control", children: bandControl }),
|
|
3851
3859
|
/* @__PURE__ */ jsx35(ThemeToggle, {}),
|
|
3852
3860
|
/* @__PURE__ */ jsx35("div", { className: "ui-app__user ui-app__user--bar", children: /* @__PURE__ */ jsx35(
|
|
3853
3861
|
Dropdown,
|
|
@@ -3876,24 +3884,7 @@ function AppShell({
|
|
|
3876
3884
|
/* @__PURE__ */ jsx35("div", { className: "ui-app__body", children })
|
|
3877
3885
|
] })
|
|
3878
3886
|
] }),
|
|
3879
|
-
navigable && /* @__PURE__ */ jsx35(
|
|
3880
|
-
links(visible),
|
|
3881
|
-
remaining.length > 0 && /* @__PURE__ */ jsx35("li", { children: /* @__PURE__ */ jsxs30(
|
|
3882
|
-
"button",
|
|
3883
|
-
{
|
|
3884
|
-
type: "button",
|
|
3885
|
-
className: `ui-nav__item${active && remaining.includes(active) ? " is-active" : ""}`,
|
|
3886
|
-
"aria-haspopup": "dialog",
|
|
3887
|
-
"aria-expanded": more,
|
|
3888
|
-
onClick: () => setMore(true),
|
|
3889
|
-
children: [
|
|
3890
|
-
/* @__PURE__ */ jsx35("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx35(Icon, { name: "moreHorizontal" }) }),
|
|
3891
|
-
/* @__PURE__ */ jsx35("span", { className: "ui-nav__label", children: "More" })
|
|
3892
|
-
]
|
|
3893
|
-
}
|
|
3894
|
-
) })
|
|
3895
|
-
] }) }),
|
|
3896
|
-
/* @__PURE__ */ jsx35(Drawer, { open: more, onClose: () => setMore(false), title: "More sections", side: "bottom", children: sidebar(remaining, "More sections") }),
|
|
3887
|
+
navigable && /* @__PURE__ */ jsx35(Drawer, { open: menu, onClose: () => setMenu(false), title: word, side: "left", size: "sm", children: /* @__PURE__ */ jsx35("div", { className: "ui-react-app__menu", children: sidebar("Menu sections") }) }),
|
|
3897
3888
|
/* @__PURE__ */ jsx35(CommandPalette, { ...palette ?? { groups: [{ items: sections.map((section) => ({ id: section.href, ...section })) }] }, open: search, onClose: () => setSearch(false) })
|
|
3898
3889
|
] });
|
|
3899
3890
|
}
|
|
@@ -3921,7 +3912,7 @@ function EmptyState({ variant = "first-run", title, sub, icon: icon7, art, actio
|
|
|
3921
3912
|
}
|
|
3922
3913
|
|
|
3923
3914
|
// src/Success.tsx
|
|
3924
|
-
import { forwardRef as forwardRef9, useEffect as useEffect15, useRef as
|
|
3915
|
+
import { forwardRef as forwardRef9, useEffect as useEffect15, useRef as useRef20, useState as useState15 } from "react";
|
|
3925
3916
|
import { jsx as jsx37, jsxs as jsxs32 } from "react/jsx-runtime";
|
|
3926
3917
|
var SuccessCheck = forwardRef9(function SuccessCheck2({ variant = "line", className, ...rest }, ref) {
|
|
3927
3918
|
const circled = variant === "circled";
|
|
@@ -3969,7 +3960,7 @@ var pieces = [
|
|
|
3969
3960
|
];
|
|
3970
3961
|
function Countdown({ seconds, label = "Redirecting", onDone }) {
|
|
3971
3962
|
const [remaining, setRemaining] = useState15(seconds);
|
|
3972
|
-
const callback =
|
|
3963
|
+
const callback = useRef20(onDone);
|
|
3973
3964
|
useEffect15(() => {
|
|
3974
3965
|
callback.current = onDone;
|
|
3975
3966
|
}, [onDone]);
|
|
@@ -4050,7 +4041,7 @@ import {
|
|
|
4050
4041
|
useId as useId13,
|
|
4051
4042
|
useLayoutEffect as useLayoutEffect5,
|
|
4052
4043
|
useMemo as useMemo4,
|
|
4053
|
-
useRef as
|
|
4044
|
+
useRef as useRef21,
|
|
4054
4045
|
useState as useState16
|
|
4055
4046
|
} from "react";
|
|
4056
4047
|
import { CHART_FLOOR, bridgeWalk, chartScale, pointTarget } from "@apliteni/apliteni-ui";
|
|
@@ -4216,16 +4207,16 @@ function Chart(props) {
|
|
|
4216
4207
|
const plotHeight = height ?? HEIGHT[variant];
|
|
4217
4208
|
const showTable = table ?? !spark;
|
|
4218
4209
|
const uid = useId13().replace(/[^\w-]/g, "");
|
|
4219
|
-
const frameEl =
|
|
4220
|
-
const scroller =
|
|
4221
|
-
const plot =
|
|
4222
|
-
const tipEl =
|
|
4223
|
-
const svgEl =
|
|
4224
|
-
const ignoreMouseUntil =
|
|
4225
|
-
const touchMoved =
|
|
4226
|
-
const dismissed =
|
|
4227
|
-
const opening =
|
|
4228
|
-
const pointerFocus =
|
|
4210
|
+
const frameEl = useRef21(null);
|
|
4211
|
+
const scroller = useRef21(null);
|
|
4212
|
+
const plot = useRef21(null);
|
|
4213
|
+
const tipEl = useRef21(null);
|
|
4214
|
+
const svgEl = useRef21(null);
|
|
4215
|
+
const ignoreMouseUntil = useRef21(0);
|
|
4216
|
+
const touchMoved = useRef21(false);
|
|
4217
|
+
const dismissed = useRef21(null);
|
|
4218
|
+
const opening = useRef21(false);
|
|
4219
|
+
const pointerFocus = useRef21(false);
|
|
4229
4220
|
const floor = useTapFloor();
|
|
4230
4221
|
const [width, setWidth] = useState16(0);
|
|
4231
4222
|
const [openId, setOpenId] = useState16(null);
|
|
@@ -4765,7 +4756,7 @@ var AccentPicker = forwardRef10(function AccentPicker2({
|
|
|
4765
4756
|
});
|
|
4766
4757
|
|
|
4767
4758
|
// src/NavTabs.tsx
|
|
4768
|
-
import { Fragment as Fragment14, forwardRef as forwardRef11, useEffect as useEffect17, useRef as
|
|
4759
|
+
import { Fragment as Fragment14, forwardRef as forwardRef11, useEffect as useEffect17, useRef as useRef22, useImperativeHandle as useImperativeHandle2 } from "react";
|
|
4769
4760
|
import { safeUrl, observeNavFit, revealCurrentNav, revealFocusedNav } from "@apliteni/apliteni-ui";
|
|
4770
4761
|
import { Fragment as Fragment15, jsx as jsx40, jsxs as jsxs34 } from "react/jsx-runtime";
|
|
4771
4762
|
var NavTabs = forwardRef11(function NavTabs2({
|
|
@@ -4778,7 +4769,7 @@ var NavTabs = forwardRef11(function NavTabs2({
|
|
|
4778
4769
|
"aria-label": ariaLabel = "Tabs",
|
|
4779
4770
|
...rest
|
|
4780
4771
|
}, ref) {
|
|
4781
|
-
const navRef =
|
|
4772
|
+
const navRef = useRef22(null);
|
|
4782
4773
|
useImperativeHandle2(ref, () => navRef.current, []);
|
|
4783
4774
|
const signature = items.map((item) => item.id).join("\0");
|
|
4784
4775
|
useEffect17(() => observeNavFit(navRef.current), []);
|
package/src/assets/icons.js
CHANGED
|
@@ -180,6 +180,10 @@ export const iconOnlyAllowed = {
|
|
|
180
180
|
// is the glyph a reader has already met doing this everywhere else. The entry
|
|
181
181
|
// is the decision — review did not infer it from the glyph looking obvious.
|
|
182
182
|
download: 'export or download the rows',
|
|
183
|
+
// The phone shell's menu control, decided in #592: below 720px the icon strip goes away
|
|
184
|
+
// and one button opens the full navigation in a drawer. The three-bar glyph is the
|
|
185
|
+
// reader's own mark for this action.
|
|
186
|
+
menu: 'open the navigation drawer',
|
|
183
187
|
};
|
|
184
188
|
|
|
185
189
|
// The word a wordless control's name may open with. The list above says which
|
|
@@ -200,6 +204,7 @@ export const iconOnlyNames = {
|
|
|
200
204
|
// depicts it, so a consumer naming it that way is not borrowing the tray for
|
|
201
205
|
// something else.
|
|
202
206
|
download: ['export', 'download'],
|
|
207
|
+
menu: ['menu'],
|
|
203
208
|
};
|
|
204
209
|
|
|
205
210
|
// What a glyph means when a component picks it for the reader rather than a
|
package/src/components/drawer.js
CHANGED
|
@@ -153,10 +153,13 @@ export function wireDrawer(root = document) {
|
|
|
153
153
|
doc.__drawerGlobalWired = true;
|
|
154
154
|
// Any [data-drawer-open="ID"] trigger opens the matching drawer.
|
|
155
155
|
doc.addEventListener('click', (e) => {
|
|
156
|
-
|
|
156
|
+
// composedPath() and not target: a trigger inside an open shadow root is retargeted
|
|
157
|
+
// to its host here, and its drawer is found in the tree the trigger is in.
|
|
158
|
+
const opener = e.composedPath().find((n) => n.nodeType === 1 && n.hasAttribute('data-drawer-open'));
|
|
157
159
|
if (!opener) return;
|
|
158
160
|
e.preventDefault();
|
|
159
|
-
const
|
|
161
|
+
const id = opener.getAttribute('data-drawer-open');
|
|
162
|
+
const target = (opener.getRootNode().getElementById?.(id)) || doc.getElementById(id);
|
|
160
163
|
if (target) openDrawer(target, opener);
|
|
161
164
|
});
|
|
162
165
|
}
|
|
@@ -51,20 +51,24 @@ export function focusablesIn(panel) {
|
|
|
51
51
|
return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(reachable);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Hide everything *outside* `root` from AT + the tab order: walk
|
|
54
|
+
// Hide everything *outside* `root` from AT + the tab order: walk through a shadow
|
|
55
|
+
// host when present, then to body, and mark each
|
|
55
56
|
// ancestor's other children. Scrim and panel live inside the root so they stay
|
|
56
57
|
// interactive; a drawer under an aria-modal alertdialog is outside it and must not be.
|
|
57
58
|
function mark(page, doc, root) {
|
|
58
59
|
let node = root;
|
|
59
|
-
while (node && node
|
|
60
|
-
|
|
60
|
+
while (node && node !== doc.body) {
|
|
61
|
+
const tree = node.getRootNode();
|
|
62
|
+
const parent = node.parentElement || (tree.host ? tree : null);
|
|
63
|
+
if (!parent) break;
|
|
64
|
+
for (const sib of parent.children) {
|
|
61
65
|
if (sib === node) continue;
|
|
62
66
|
page.marked.push([sib, sib.getAttribute('aria-hidden'), sib.hasAttribute('inert')]);
|
|
63
67
|
sib.setAttribute('aria-hidden', 'true');
|
|
64
68
|
sib.setAttribute('inert', '');
|
|
65
69
|
sib.inert = true;
|
|
66
70
|
}
|
|
67
|
-
node =
|
|
71
|
+
node = parent.host || parent;
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -248,7 +252,7 @@ export function trapTab(panel, e) {
|
|
|
248
252
|
if (!items.length) { e.preventDefault(); panel.focus(); return; }
|
|
249
253
|
const first = items[0];
|
|
250
254
|
const last = items[items.length - 1];
|
|
251
|
-
const active =
|
|
255
|
+
const active = panel.getRootNode().activeElement || panel.ownerDocument.activeElement;
|
|
252
256
|
if (!panel.contains(active)) {
|
|
253
257
|
e.preventDefault(); (e.shiftKey ? last : first).focus();
|
|
254
258
|
} else if (e.shiftKey && (active === first || active === panel)) {
|
package/src/components/shell.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// wireShell() once after mounting wires the fold, the nav's groups and the reader's menu.
|
|
4
4
|
// why: docs/components.md#the-page-shell
|
|
5
5
|
import { topbar as productTopbar } from './topbar.js';
|
|
6
|
-
import { esc, icon } from './index.js';
|
|
6
|
+
import { esc, icon, button } from './index.js';
|
|
7
7
|
import { sidebarNav, breadcrumbs, wireNav } from './nav.js';
|
|
8
8
|
import { dropdown, wireDropdown } from './dropdown.js';
|
|
9
|
+
import { drawer, wireDrawer, closeDrawer } from './drawer.js';
|
|
9
10
|
import { backLink } from './back.js';
|
|
10
11
|
import { prism } from '../assets/brand.js';
|
|
11
12
|
import { ACCOUNT_NAV, toMenuTuple, initials } from './account-nav.js';
|
|
@@ -207,7 +208,7 @@ const readerFace = (name, email, named, markOnly = false) =>
|
|
|
207
208
|
//
|
|
208
209
|
// One block, two places: only the direction it opens is the layout's answer.
|
|
209
210
|
// `portal: true` either way — the rail and the band are both sticky.
|
|
210
|
-
function readerBlock({ name, email }, signOutHref, { band = false } = {}) {
|
|
211
|
+
function readerBlock({ name, email }, signOutHref, { band = false, portal = true } = {}) {
|
|
211
212
|
if (!name && !email) return '';
|
|
212
213
|
const cls = `ui-app__user${band ? ' ui-app__user--bar' : ''}`;
|
|
213
214
|
const who = [name, email].filter(Boolean).join(', ');
|
|
@@ -225,7 +226,7 @@ function readerBlock({ name, email }, signOutHref, { band = false } = {}) {
|
|
|
225
226
|
+ `</div>`;
|
|
226
227
|
return `<div class="${cls}">${dropdown({
|
|
227
228
|
variant: 'menu',
|
|
228
|
-
portal
|
|
229
|
+
portal,
|
|
229
230
|
direction: band ? 'down' : 'up',
|
|
230
231
|
align: band ? 'end' : 'start',
|
|
231
232
|
chevron: !band,
|
|
@@ -253,13 +254,26 @@ const searchField = ({ palette, placeholder, shortLabel }) =>
|
|
|
253
254
|
|
|
254
255
|
// The band: search at its start, the reader at its end, beside the rail rather than
|
|
255
256
|
// across the top of both. why: docs/components.md#the-page-shell
|
|
256
|
-
const shellBar = (search, account, signOutHref) =>
|
|
257
|
+
const shellBar = (search, account, signOutHref, lead = '') =>
|
|
257
258
|
`<header class="ui-app__bar">`
|
|
259
|
+
+ lead
|
|
258
260
|
+ (search ? searchField(search) : '')
|
|
259
261
|
+ `<span class="ui-app__bar-gap"></span>`
|
|
260
262
|
+ readerBlock(account, signOutHref, { band: true })
|
|
261
263
|
+ `</header>`;
|
|
262
264
|
|
|
265
|
+
// Below 720px the rail is not drawn. A band holds the menu control and the brand's mark,
|
|
266
|
+
// and the control opens the same rail in the kit's left drawer. Each part carries
|
|
267
|
+
// `ui-app__phone`, so layout.css draws it at that width only.
|
|
268
|
+
// why: docs/components.md#the-page-shell
|
|
269
|
+
const menuControl = (id) => button({ label: 'Menu', variant: 'ghost', icon: 'menu', iconOnly: true })
|
|
270
|
+
.replace('<button ', `<button data-drawer-open="${id}" aria-haspopup="dialog" `)
|
|
271
|
+
.replace('class="', 'class="ui-app__menu ');
|
|
272
|
+
|
|
273
|
+
const bandBrand = (word, href, uid) =>
|
|
274
|
+
`<a class="ui-app__brand ui-app__brand--band" href="${esc(safeUrl(href))}" aria-label="${esc(word)}">`
|
|
275
|
+
+ `${prism(`appb-${uid}`, 24)}</a>`;
|
|
276
|
+
|
|
263
277
|
// Unique-per-render suffix for the brand mark's clip id — the same reason
|
|
264
278
|
// nav.js keeps a module counter. Two shells on one page must not collide.
|
|
265
279
|
let _shellUid = 0;
|
|
@@ -291,6 +305,7 @@ export function appShell(options = {}) {
|
|
|
291
305
|
// The layout's one branch, read once. Everything below asks this and not the string.
|
|
292
306
|
const banded = layout === 'topbar';
|
|
293
307
|
const up = back ? backLink(back) : '';
|
|
308
|
+
const menuId = `ui-app-menu-${++_shellUid}`;
|
|
294
309
|
// No footer slot: the nav is places to go, and the one thing that was in it —
|
|
295
310
|
// sign out — is in the reader's menu at the rail's foot.
|
|
296
311
|
const rail = sidebarNav({
|
|
@@ -331,6 +346,27 @@ export function appShell(options = {}) {
|
|
|
331
346
|
const foot = banded
|
|
332
347
|
? (fold ? `<div class="ui-app__foot">${fold}</div>` : '')
|
|
333
348
|
: readerBlock(account, signOutHref);
|
|
349
|
+
// The phone's way to the same rail: the menu control, and the drawer it opens. The
|
|
350
|
+
// drawer holds a second copy of the nav and the reader, because a drawer is a dialog
|
|
351
|
+
// and the rail is a landmark; layout.css draws one or the other. The banded layout
|
|
352
|
+
// keeps its reader on the band, so its drawer has no foot.
|
|
353
|
+
const menu = menuControl(menuId);
|
|
354
|
+
const mark = bandBrand(word, brandHref, ++_shellUid);
|
|
355
|
+
const phone = `<div class="ui-app__drawer">${drawer({
|
|
356
|
+
side: 'left',
|
|
357
|
+
size: 'sm',
|
|
358
|
+
title: word,
|
|
359
|
+
id: menuId,
|
|
360
|
+
body: sidebarNav({
|
|
361
|
+
sections: [{ label: navLabel, items: nav }],
|
|
362
|
+
active,
|
|
363
|
+
activeIs: up ? 'section' : 'page',
|
|
364
|
+
ariaLabel: navLabel,
|
|
365
|
+
}),
|
|
366
|
+
// Not portalled: a panel lifted to the page would sit outside the open drawer, inert
|
|
367
|
+
// and under it, so Sign out could not be reached.
|
|
368
|
+
footer: banded ? '' : readerBlock(account, signOutHref, { portal: false }),
|
|
369
|
+
})}</div>`;
|
|
334
370
|
const main = `<main class="ui-app__main${width === 'wide' ? ' ui-app__main--wide' : ''}"${maxWidth ? ` style="--ui-app-main: ${maxWidth}"` : ''}>
|
|
335
371
|
${up || (crumbs.length ? breadcrumbs({ items: crumbs }) : '')}
|
|
336
372
|
${title ? `<h1>${title}</h1>` : ''}
|
|
@@ -339,8 +375,8 @@ export function appShell(options = {}) {
|
|
|
339
375
|
</main>`;
|
|
340
376
|
// The second column is a box of its own: the band and the page are two rows of it.
|
|
341
377
|
const well = banded
|
|
342
|
-
? `<div class="ui-app__well">${shellBar(search, account, signOutHref)}${main}</div>`
|
|
343
|
-
: main
|
|
378
|
+
? `<div class="ui-app__well">${shellBar(search, account, signOutHref, menu + mark)}${main}</div>`
|
|
379
|
+
: `${compat ? '' : `<header class="ui-app__bar ui-app__bar--phone">${menu}${mark}</header>`}${main}`;
|
|
344
380
|
const grid = `<div class="ui-app${banded ? ' ui-app--topbar' : ''}${folded ? ' is-collapsed' : ''}"${auto}>
|
|
345
381
|
<div class="ui-app__rail" role="region" aria-label="Sidebar">
|
|
346
382
|
${head}
|
|
@@ -348,8 +384,11 @@ export function appShell(options = {}) {
|
|
|
348
384
|
${foot}
|
|
349
385
|
</div>
|
|
350
386
|
${well}
|
|
387
|
+
${phone}
|
|
351
388
|
</div>`;
|
|
352
|
-
return compat
|
|
389
|
+
return compat
|
|
390
|
+
? `<div class="ui-app-page">${productTopbar(compat).replace('<div class="topbar__in">', `<div class="topbar__in">${menu}`)}${grid}</div>`
|
|
391
|
+
: grid;
|
|
353
392
|
}
|
|
354
393
|
|
|
355
394
|
// ---- Behaviour -----------------------------------------------------------
|
|
@@ -358,6 +397,9 @@ export function appShell(options = {}) {
|
|
|
358
397
|
// keeps every shell under root out of the cookie, shells drawn there later
|
|
359
398
|
// included; only `persist: true` on that root turns it back on.
|
|
360
399
|
const _wiredDocs = new WeakSet();
|
|
400
|
+
// The roots wireShell() was handed, per document: a resize has to find a drawer open
|
|
401
|
+
// inside a shadow root, which the document's own querySelectorAll does not enter.
|
|
402
|
+
const _wiredRoots = new WeakMap();
|
|
361
403
|
const _unpersisted = new WeakSet();
|
|
362
404
|
|
|
363
405
|
// The toggle, addressed from the rail that owns it: a fold row of a shell's own rail
|
|
@@ -369,6 +411,8 @@ const _unpersisted = new WeakSet();
|
|
|
369
411
|
const FOLD_PATH = '.ui-app__fold-row > [data-rail-toggle]';
|
|
370
412
|
const RAIL_FOLD = `.ui-app__rail ${FOLD_PATH}`;
|
|
371
413
|
|
|
414
|
+
const DRAWER_LINK = '.ui-app__drawer a.ui-nav__item[href]';
|
|
415
|
+
|
|
372
416
|
// Under an opted-out root? Steps out of a shadow root through its host.
|
|
373
417
|
const optedOut = (node) => {
|
|
374
418
|
for (let n = node; n; n = n.parentNode || n.host) if (_unpersisted.has(n)) return true;
|
|
@@ -390,13 +434,31 @@ function setRail(app, collapsed) {
|
|
|
390
434
|
function listen(doc) {
|
|
391
435
|
if (_wiredDocs.has(doc)) return;
|
|
392
436
|
_wiredDocs.add(doc);
|
|
437
|
+
// A drawer left open while the window grows past the phone width would be hidden and
|
|
438
|
+
// still hold the page inert, so it closes there. Its wrapper is not drawn above that
|
|
439
|
+
// width, which is how the stylesheet's breakpoint is read without a second copy of it.
|
|
440
|
+
const view = doc.defaultView;
|
|
441
|
+
if (view) {
|
|
442
|
+
view.addEventListener('resize', () => {
|
|
443
|
+
for (const r of _wiredRoots.get(doc) || []) {
|
|
444
|
+
r.querySelectorAll('.ui-app__drawer .ui-drawer.is-open').forEach((d) => {
|
|
445
|
+
if (view.getComputedStyle(d.parentElement).display === 'none') closeDrawer(d);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
393
450
|
doc.addEventListener('click', (e) => {
|
|
394
451
|
// A shell's own toggle only, in the head of its own rail: a stray
|
|
395
452
|
// [data-rail-toggle] in the page body folds nothing. composedPath() rather
|
|
396
453
|
// than target, so a shell inside an open shadow root is found. closest()
|
|
397
454
|
// rather than a chain of parents, which the head band made one link longer
|
|
398
455
|
// and which said nothing about what it was walking through.
|
|
399
|
-
const
|
|
456
|
+
const path = e.composedPath();
|
|
457
|
+
// A press on a place in the phone drawer closes it. A group's toggle is a button
|
|
458
|
+
// and goes nowhere, so only a link does. why: docs/components.md#the-page-shell
|
|
459
|
+
const row = path.find((n) => n.nodeType === 1 && n.matches(DRAWER_LINK));
|
|
460
|
+
if (row) closeDrawer(row.closest('.ui-drawer'));
|
|
461
|
+
const btn = path.find((n) => n.nodeType === 1 && n.matches(RAIL_FOLD));
|
|
400
462
|
const app = btn && btn.closest('.ui-app');
|
|
401
463
|
if (!app) return;
|
|
402
464
|
const next = !app.classList.contains('is-collapsed');
|
|
@@ -408,7 +470,6 @@ function listen(doc) {
|
|
|
408
470
|
} catch (err) { /* a sandboxed frame: the fold works, nothing is kept */ }
|
|
409
471
|
}
|
|
410
472
|
// A document with no window (DOMParser, createHTMLDocument) has no CustomEvent to send.
|
|
411
|
-
const view = doc.defaultView;
|
|
412
473
|
if (view) {
|
|
413
474
|
app.dispatchEvent(new view.CustomEvent('ui-rail', { bubbles: true, composed: true, detail: { collapsed: next } }));
|
|
414
475
|
}
|
|
@@ -420,6 +481,7 @@ export function wireShell(root = document, { persist } = {}) {
|
|
|
420
481
|
// The reader's menu is a dropdown() like any other, so it is wired like any
|
|
421
482
|
// other. Idempotent, and a shell with no account draws none to find.
|
|
422
483
|
wireDropdown(root);
|
|
484
|
+
wireDrawer(root);
|
|
423
485
|
// A server cannot know which key the reader holds, so the browser corrects it here —
|
|
424
486
|
// off the ROOT's window, not the global one. why: docs/components.md#the-page-shell
|
|
425
487
|
const here = root.nodeType === 9 ? root : root.ownerDocument;
|
|
@@ -428,6 +490,10 @@ export function wireShell(root = document, { persist } = {}) {
|
|
|
428
490
|
for (const kbd of root.querySelectorAll('[data-palette-hotkey]')) kbd.textContent = key;
|
|
429
491
|
const doc = root.nodeType === 9 ? root : root.ownerDocument;
|
|
430
492
|
listen(doc);
|
|
493
|
+
if (!_wiredRoots.has(doc)) _wiredRoots.set(doc, new Set());
|
|
494
|
+
const roots = _wiredRoots.get(doc);
|
|
495
|
+
for (const r of roots) if (r !== doc && !(r.host || r).isConnected) roots.delete(r);
|
|
496
|
+
roots.add(root);
|
|
431
497
|
if (persist === false) _unpersisted.add(root);
|
|
432
498
|
else if (persist === true) _unpersisted.delete(root);
|
|
433
499
|
const saved = railCollapsed(cookieOf(doc));
|
package/src/components/toasts.js
CHANGED
|
@@ -16,6 +16,9 @@ import { toast } from './index.js';
|
|
|
16
16
|
const reduceMotion = () =>
|
|
17
17
|
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
18
18
|
|
|
19
|
+
const resolve = (container) =>
|
|
20
|
+
(typeof container === 'string' ? document.querySelector(container) : container) || null;
|
|
21
|
+
|
|
19
22
|
// Slide-and-fade a toast out, then remove it. Idempotent.
|
|
20
23
|
export function dismissToast(el) {
|
|
21
24
|
if (!el || el.dataset.leaving) return;
|
|
@@ -36,20 +39,41 @@ function wireToast(el) {
|
|
|
36
39
|
el.querySelector('.ui-toast__close')?.addEventListener('click', () => dismissToast(el));
|
|
37
40
|
|
|
38
41
|
const bar = el.querySelector('[data-toast-timer]');
|
|
39
|
-
if (bar
|
|
42
|
+
if (bar) {
|
|
40
43
|
bar.classList.add('is-running');
|
|
41
44
|
const dur = parseFloat(getComputedStyle(el).getPropertyValue('--toast-dur')) || 5;
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
let remaining = dur * 1000;
|
|
46
|
+
let started = Date.now();
|
|
47
|
+
let timer = setTimeout(() => dismissToast(el), remaining);
|
|
48
|
+
// The line's own pause lives in the sheet, on :hover and :focus-within, so
|
|
49
|
+
// it covers a keyboard reader too. What is left here is the countdown that
|
|
50
|
+
// has to stop with it. This runs even under reduced motion, where the line
|
|
51
|
+
// is hidden but the reader still gets the same hold.
|
|
52
|
+
let held = false;
|
|
53
|
+
const hold = () => {
|
|
54
|
+
if (held) return;
|
|
55
|
+
held = true;
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
const visualLeft = !reduceMotion()
|
|
58
|
+
? parseFloat(getComputedStyle(bar).transform.split(',')[0].slice(7))
|
|
59
|
+
: NaN;
|
|
60
|
+
remaining = Number.isFinite(visualLeft)
|
|
61
|
+
? dur * 1000 * visualLeft
|
|
62
|
+
: Math.max(0, remaining - (Date.now() - started));
|
|
63
|
+
};
|
|
64
|
+
const release = () => {
|
|
65
|
+
if (!held || el.matches(':hover, :focus-within')) return;
|
|
66
|
+
held = false;
|
|
67
|
+
started = Date.now();
|
|
68
|
+
timer = setTimeout(() => dismissToast(el), remaining);
|
|
69
|
+
};
|
|
70
|
+
el.addEventListener('mouseenter', hold);
|
|
71
|
+
el.addEventListener('mouseleave', release);
|
|
72
|
+
el.addEventListener('focusin', hold);
|
|
73
|
+
// At focusout the old target has already blurred and the new one has not
|
|
74
|
+
// focused yet, so :focus-within reads false even for a move from the close
|
|
75
|
+
// button to the action. relatedTarget is the only thing that knows.
|
|
76
|
+
el.addEventListener('focusout', (e) => { if (!el.contains(e.relatedTarget)) release(); });
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
// Swipe-to-dismiss (ignore drags that start on a button).
|
|
@@ -77,7 +101,7 @@ function wireToast(el) {
|
|
|
77
101
|
|
|
78
102
|
// Wire every toast currently inside a container (string selector or element).
|
|
79
103
|
export function wireToastStack(container) {
|
|
80
|
-
const root =
|
|
104
|
+
const root = resolve(container);
|
|
81
105
|
if (!root) return null;
|
|
82
106
|
root.querySelectorAll('.ui-toast').forEach(wireToast);
|
|
83
107
|
return root;
|
|
@@ -85,7 +109,7 @@ export function wireToastStack(container) {
|
|
|
85
109
|
|
|
86
110
|
// Build a toast from opts, prepend it to a live stack (newest on top), wire it.
|
|
87
111
|
export function pushToast(container, opts = {}) {
|
|
88
|
-
const root =
|
|
112
|
+
const root = resolve(container);
|
|
89
113
|
if (!root) return null;
|
|
90
114
|
root.insertAdjacentHTML('afterbegin', toast(opts));
|
|
91
115
|
const el = root.firstElementChild;
|
package/src/styles/callout.css
CHANGED
|
@@ -93,9 +93,16 @@
|
|
|
93
93
|
.ui-toast__close:hover { color: var(--text); }
|
|
94
94
|
.ui-toast__close:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); border-radius: var(--radius-xs); }
|
|
95
95
|
|
|
96
|
-
/* auto-dismiss progress
|
|
96
|
+
/* auto-dismiss progress line — .is-running is added by wireToastStack() */
|
|
97
97
|
.ui-toast__timer { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: var(--toast-accent); opacity: 0.85; transform-origin: left; }
|
|
98
|
-
.ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; /* motion: ambient — the
|
|
98
|
+
.ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; /* motion: ambient — the line spends the toast's dismiss timer, which the caller sets */ }
|
|
99
|
+
/* The line spends the notice's own timer, so it stops exactly when the timer
|
|
100
|
+
does: under the pointer, and while focus is anywhere inside. Holding both
|
|
101
|
+
pauses here rather than in each face's event wiring keeps the line and the
|
|
102
|
+
countdown from drifting apart, and gives a keyboard reader the same reprieve
|
|
103
|
+
a pointer reader gets. */
|
|
104
|
+
.ui-toast:hover > .ui-toast__timer.is-running,
|
|
105
|
+
.ui-toast:focus-within > .ui-toast__timer.is-running { animation-play-state: paused; }
|
|
99
106
|
|
|
100
107
|
/* Legacy style classes share the neutral card. */
|
|
101
108
|
.ui-toast--soft, .ui-toast--solid, .ui-toast--outline { background: var(--surface); color: var(--text); }
|
|
@@ -110,7 +117,10 @@
|
|
|
110
117
|
|
|
111
118
|
@media (prefers-reduced-motion: reduce) {
|
|
112
119
|
.ui-toast, .ui-toast.is-leaving { animation: none; }
|
|
113
|
-
|
|
120
|
+
/* A line that cannot move sits full for the whole five seconds and reads as
|
|
121
|
+
"no time spent yet" — the opposite of what it is there to say. The notice
|
|
122
|
+
still expires on time; only the false readout goes. */
|
|
123
|
+
.ui-toast__timer { display: none; }
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
/* stack container for queued toasts */
|
package/src/styles/layout.css
CHANGED
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
resting step. */
|
|
120
120
|
/* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
|
|
121
121
|
the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
|
|
122
|
-
stories/apps/shell-states.test.js:
|
|
122
|
+
stories/apps/shell-states.test.js:1535 `a resting glyph is dimmer but still legible`. */
|
|
123
123
|
.ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
|
|
124
124
|
|
|
125
125
|
/* A destructive row of the rail is a navigation row and rests like one; the danger
|
|
@@ -139,6 +139,8 @@
|
|
|
139
139
|
padding-top: var(--space-5);
|
|
140
140
|
border-top: 1px solid var(--border);
|
|
141
141
|
}
|
|
142
|
+
/* A drawer's footer carries no rule above it, so the block keeps the rail's rule out. */
|
|
143
|
+
.ui-app__drawer .ui-app__user { padding-top: 0; border-top: 0; }
|
|
142
144
|
/* The other thing that can close a rail: where the reader's block is in the band,
|
|
143
145
|
the fold's control takes its place under the same rule.
|
|
144
146
|
why: docs/components.md#the-page-shell */
|
|
@@ -254,8 +256,7 @@
|
|
|
254
256
|
.ui-app.is-collapsed .ui-app__fold .ui-app__fold-arrow { transform: scaleX(-1); }
|
|
255
257
|
|
|
256
258
|
/* -- The collapsed rail --------------------------------------------------- */
|
|
257
|
-
/*
|
|
258
|
-
again. why: docs/components.md#the-page-shell */
|
|
259
|
+
/* The reader's own fold, at the widths that have a rail. why: docs/components.md#the-page-shell */
|
|
259
260
|
.ui-app.is-collapsed { --ui-rail-w: calc(var(--ui-nav-strip) + 2 * var(--space-4) + var(--ui-rail-rule)); }
|
|
260
261
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__cap,
|
|
261
262
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__label,
|
|
@@ -263,20 +264,17 @@
|
|
|
263
264
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__caret,
|
|
264
265
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-app__who,
|
|
265
266
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-dropdown__chevron,
|
|
266
|
-
:where(.ui-app.is-collapsed) .ui-app__brand span { opacity: 0; pointer-events: none; }
|
|
267
|
+
:where(.ui-app.is-collapsed) .ui-app__rail .ui-app__brand span { opacity: 0; pointer-events: none; }
|
|
267
268
|
/* The toggle rides onto the glyph column — the two widths' own difference — and the
|
|
268
|
-
lockup goes whole, since that column is where its mark stood.
|
|
269
|
-
|
|
270
|
-
gives the lockup back rather than leaving an empty band with a hairline under it.
|
|
271
|
-
why: docs/components.md#the-page-shell */
|
|
269
|
+
lockup goes whole, since that column is where its mark stood. Below 720px there is
|
|
270
|
+
no rail, so none of this is drawn. why: docs/components.md#the-page-shell */
|
|
272
271
|
:where(.ui-app.is-collapsed) .ui-app__head > .ui-app__fold-row { inset-inline-start: calc(var(--ui-nav-strip) - var(--ui-nav-col)); }
|
|
273
272
|
/* …and only in the layout where something arrives on the mark's column. In the
|
|
274
273
|
topbar layout the toggle is at the rail's foot, nothing lands where the mark
|
|
275
274
|
stands, and a lockup faded out there leaves a band with nothing in it and its
|
|
276
|
-
hairline still under it
|
|
277
|
-
that layout keeps the mark and lets the shared rule above fold away its words.
|
|
275
|
+
hairline still under it. So that layout keeps the mark and lets the shared rule above fold away its words.
|
|
278
276
|
why: docs/components.md#the-page-shell */
|
|
279
|
-
:where(.ui-app:not(.ui-app--topbar).is-collapsed) .ui-app__brand { opacity: 0; visibility: hidden; pointer-events: none; }
|
|
277
|
+
:where(.ui-app:not(.ui-app--topbar).is-collapsed) .ui-app__rail .ui-app__brand { opacity: 0; visibility: hidden; pointer-events: none; }
|
|
280
278
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
|
|
281
279
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
|
|
282
280
|
/* The current row's plate on the glyph column, and beside it the one thing a bare rail
|
|
@@ -502,61 +500,48 @@
|
|
|
502
500
|
.ui-toolbar--split > :last-child { margin-inline-start: 0; align-self: start; }
|
|
503
501
|
}
|
|
504
502
|
|
|
505
|
-
/* -- The
|
|
506
|
-
/* Below this width
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
503
|
+
/* -- The phone shell ------------------------------------------------------ */
|
|
504
|
+
/* Below this width there is no rail and no strip. A band at the top holds the menu
|
|
505
|
+
control and the brand's mark, the control opens the same rail in the kit's left
|
|
506
|
+
drawer, and the reading column takes the whole width. The band, the control and the
|
|
507
|
+
drawer exist at every width and are not drawn above it, so the rail's rows above
|
|
508
|
+
720px are the rows they were. why: docs/components.md#the-page-shell */
|
|
509
|
+
.ui-app__bar--phone { display: none; }
|
|
510
|
+
.ui-btn.ui-app__menu { display: none; }
|
|
511
|
+
.ui-app__brand--band { display: none; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; /* a finger's box around a 24px mark */ }
|
|
512
|
+
.ui-app__drawer { display: none; }
|
|
513
|
+
/* The drawer panel is --bg-elevated, which --surface matches exactly in light, so nav.css's
|
|
514
|
+
plate measures 1.00:1 there. The current row and the hover wash take the rung the rail
|
|
515
|
+
takes (--surface-3), and the count keeps its --surface pill on that plate. Dark already
|
|
516
|
+
has a step and keeps it. why: docs/components.md#the-page-shell */
|
|
517
|
+
:where(:root[data-theme="light"]) .ui-app__drawer .ui-nav__item:not(.is-danger):not(.is-active):hover { background: color-mix(in srgb, var(--surface-3) 50%, transparent); /* code-bg: inherit — The wash keeps the containing ground. */ }
|
|
518
|
+
:where(:root[data-theme="light"]) .ui-app__drawer .ui-nav__item.is-active { background: var(--surface-3); /* code-bg: inherit — The control keeps the containing ground. */ }
|
|
511
519
|
@media (max-width: 720px) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
.ui-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
.ui-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
.ui-
|
|
525
|
-
/*
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
.ui-
|
|
532
|
-
.ui-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
.ui-
|
|
536
|
-
.ui-
|
|
537
|
-
.ui-app__rail .ui-dropdown__chevron,
|
|
538
|
-
.ui-app__brand span { opacity: 0; pointer-events: none; }
|
|
539
|
-
.ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
|
|
540
|
-
.ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
|
|
541
|
-
.ui-app__rail .ui-nav__item.is-active { isolation: isolate; background: none; }
|
|
542
|
-
.ui-app__rail .ui-nav__item.is-active::before { content: ""; position: absolute; left: 0; top: 50%; width: var(--ui-nav-strip); height: 100%; border-radius: 10px; transform: translateY(-50%); /* motion: still — a mark on the current row, whose own highlight already transitions; this centres it, it does not travel */ background: var(--surface-3); /* code-bg: inherit — The plate contains no focus target. */ z-index: -1; }
|
|
543
|
-
.ui-app__rail .ui-nav__item:not(.is-danger):not(.is-active):hover { background: none; }
|
|
544
|
-
/* The one line of this block that is deliberately NOT the reader's fold: a finger
|
|
545
|
-
is the only pointer this width has, so a row and the account block are held to
|
|
546
|
-
44px. Neither floor can spread to the press, where growing the box is the one
|
|
547
|
-
thing the travel promises nothing does. why: docs/components.md#the-page-shell */
|
|
548
|
-
.ui-app__rail .ui-nav__item,
|
|
549
|
-
.ui-app__rail .ui-app__user-trigger { min-height: 44px; }
|
|
550
|
-
.ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
|
|
551
|
-
.ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who { position: fixed; left: calc(var(--ui-rail-w) + var(--space-2)); z-index: var(--z-dropdown); width: max-content; max-width: 15rem; margin-block: -7px; padding: 6px 10px; border: 1px solid var(--float-edge); box-shadow: inset 0 0 0 1px var(--elev-edge, var(--float-edge-inner)), var(--elev-drop); border-radius: var(--radius-xs); background: var(--surface); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ color: var(--strong); opacity: 1; pointer-events: none; }
|
|
552
|
-
@supports (anchor-name: --a) and (anchor-scope: --a) {
|
|
553
|
-
.ui-app { anchor-scope: --ui-rail; }
|
|
554
|
-
.ui-app__rail { anchor-name: --ui-rail; }
|
|
555
|
-
.ui-app__rail .ui-nav__item,
|
|
556
|
-
.ui-app__rail .ui-app__user-trigger { anchor-name: --ui-rail-row; anchor-scope: --ui-rail-row; }
|
|
557
|
-
.ui-app__rail .ui-nav__item:is(:hover, :focus-visible) > .ui-nav__label,
|
|
558
|
-
.ui-app__rail .ui-app__user-trigger:is(:hover, :focus-visible) > .ui-app__who { position-anchor: --ui-rail-row; left: calc(anchor(--ui-rail right) + var(--space-2)); top: anchor(top); bottom: anchor(bottom); align-self: center; margin-block: 0; }
|
|
559
|
-
}
|
|
520
|
+
/* A column, not a grid: the band sticks to the top of the shell, and a sticky grid
|
|
521
|
+
item sticks only inside its own row. */
|
|
522
|
+
.ui-app { display: flex; flex-direction: column; }
|
|
523
|
+
.ui-app > .ui-app__rail { display: none; }
|
|
524
|
+
.ui-app__well { flex: 1 1 auto; }
|
|
525
|
+
.ui-app > .ui-app__main { flex: 1 1 auto; }
|
|
526
|
+
.ui-app > .ui-app__bar--phone { display: flex; }
|
|
527
|
+
.ui-btn.ui-app__menu { display: inline-flex; }
|
|
528
|
+
.ui-app__brand--band { display: flex; }
|
|
529
|
+
/* The menu control stands at the band's start with the band's inset beside it. */
|
|
530
|
+
.ui-app__bar { padding-inline: var(--space-2) var(--space-4); }
|
|
531
|
+
/* The drawer paints itself and sits out of the flow, so its wrapper takes no height. */
|
|
532
|
+
.ui-app__drawer { display: block; }
|
|
533
|
+
/* The rail's rows are the drawer's rows: the column fills the panel, and the foot
|
|
534
|
+
starts at the panel's edge like the rows above it. */
|
|
535
|
+
.ui-app__drawer .ui-nav--side,
|
|
536
|
+
.ui-app__drawer .ui-nav--side > * { width: 100%; }
|
|
537
|
+
/* The panel's title names the list, so the section's own caption stays out. */
|
|
538
|
+
.ui-app__drawer .ui-nav__cap { display: none; }
|
|
539
|
+
.ui-app__drawer .ui-drawer__footer { justify-content: flex-start; }
|
|
540
|
+
.ui-app__drawer .ui-app__user { width: 100%; }
|
|
541
|
+
/* A finger is the only pointer this width has, so a row and the account block are
|
|
542
|
+
held to 44px. why: docs/components.md#the-page-shell */
|
|
543
|
+
.ui-app__drawer .ui-nav__item,
|
|
544
|
+
.ui-app__drawer .ui-app__user-trigger { min-height: 44px; }
|
|
560
545
|
.ui-app__main { padding: 28px var(--space-4) 56px; }
|
|
561
546
|
.ui-app__main h1 { font-size: calc(var(--text-xl) + 3px); }
|
|
562
547
|
/* A settings row is label-beside-control down to the width where the control
|
|
@@ -733,16 +718,3 @@
|
|
|
733
718
|
forced-color-adjust: auto; background: Canvas; color: CanvasText; border-color: CanvasText;
|
|
734
719
|
}
|
|
735
720
|
}
|
|
736
|
-
@media (forced-colors: active) and (max-width: 720px) {
|
|
737
|
-
.ui-app .ui-app__rail .ui-nav__item:is(.is-active, .is-current) { isolation: isolate; background: none !important; }
|
|
738
|
-
.ui-app .ui-app__rail .ui-nav__item.is-current:not(.is-active)::before {
|
|
739
|
-
content: ""; position: absolute; left: 0; top: 50%; width: var(--ui-nav-strip); height: 100%; border-radius: 10px;
|
|
740
|
-
transform: translateY(-50%);
|
|
741
|
-
z-index: -1;
|
|
742
|
-
}
|
|
743
|
-
.ui-app .ui-app__rail .ui-nav__item.is-current:not(.is-active):not(.is-disabled)::before { background: Highlight; }
|
|
744
|
-
.ui-app .ui-app__rail .ui-nav__item.is-current.is-disabled:not(.is-active)::before { background: GrayText; }
|
|
745
|
-
.ui-app .ui-app__rail .ui-nav__item:is(.is-active, .is-current):is(:hover, :focus-visible) > .ui-nav__label {
|
|
746
|
-
forced-color-adjust: auto; background: Canvas; color: CanvasText; border-color: CanvasText;
|
|
747
|
-
}
|
|
748
|
-
}
|
package/src/styles/topbar.css
CHANGED
|
@@ -252,6 +252,15 @@
|
|
|
252
252
|
padding: var(--space-1) var(--space-3) 3px;
|
|
253
253
|
}
|
|
254
254
|
.topbar__in > .brand { grid-area: 1 / 1 / 2 / 3; justify-self: start; }
|
|
255
|
+
/* With the shell's menu control on the band it takes the first column and the brand
|
|
256
|
+
the next, so the switch row below spans both. */
|
|
257
|
+
.topbar__in:has(> .ui-app__menu) { grid-template-columns: max-content max-content minmax(0, 1fr) auto auto; }
|
|
258
|
+
.topbar__in > .ui-app__menu { grid-area: 1 / 1; margin-inline-end: var(--space-2); }
|
|
259
|
+
.topbar__in:has(> .ui-app__menu) > .brand { grid-area: 1 / 2 / 2 / 4; }
|
|
260
|
+
.topbar__in:has(> .ui-app__menu) > .toggle { grid-area: 1 / 4; }
|
|
261
|
+
.topbar__in:has(> .ui-app__menu) > .acct { grid-area: 1 / 5; }
|
|
262
|
+
.topbar__in:has(> .ui-app__menu) > .dtsw { grid-area: 2 / 1 / 3 / 3; }
|
|
263
|
+
.topbar__in:has(> .ui-app__menu) > .vsw { grid-area: 2 / 3 / 3 / -1; }
|
|
255
264
|
.topbar__in > .toggle { grid-area: 1 / 3; justify-self: end; }
|
|
256
265
|
/* The 8px beside the account is its own margin, so a hidden account leaves no gap and
|
|
257
266
|
the theme switch ends on the inset. */
|