@apliteni/apliteni-ui 0.61.2 → 0.62.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/README.md +6 -0
- package/package.json +1 -1
- package/react/dist/index.d.ts +51 -1
- package/react/dist/index.js +181 -62
- package/src/components/shell.js +10 -5
- package/src/styles/layout.css +40 -10
- package/src/styles/nav.css +50 -11
package/README.md
CHANGED
|
@@ -220,6 +220,12 @@ react/ # React components — private workspace, built to reac
|
|
|
220
220
|
dist/ # tsup output; shipped as …/react and …/react/css
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
+
`SidebarNav` renders flat items or captioned sections with one level of nested
|
|
224
|
+
disclosure, counts, disabled and danger rows, an artwork slot, and a footer slot. Its
|
|
225
|
+
`renderLink(item, linkProps)` hook supports router links; spread the supplied props
|
|
226
|
+
to retain names, children and current-page semantics. `collapsed` changes only
|
|
227
|
+
presentation, keeping nested links reachable and named.
|
|
228
|
+
|
|
223
229
|
## Contribute
|
|
224
230
|
|
|
225
231
|
Use Node 20 or newer. Install [jq](https://jqlang.github.io/jq/), then run `npm ci` at
|
package/package.json
CHANGED
package/react/dist/index.d.ts
CHANGED
|
@@ -659,7 +659,57 @@ type TooltipProps = {
|
|
|
659
659
|
};
|
|
660
660
|
declare function Tooltip({ text, children }: TooltipProps): react.JSX.Element;
|
|
661
661
|
|
|
662
|
+
type SidebarNavBadge = string | number | {
|
|
663
|
+
text: string | number;
|
|
664
|
+
tone?: 'neutral' | 'accent' | 'live' | 'danger';
|
|
665
|
+
};
|
|
666
|
+
/** A row that goes somewhere. A group takes these as its children. */
|
|
667
|
+
type SidebarNavLeaf = {
|
|
668
|
+
id: string;
|
|
669
|
+
label: string;
|
|
670
|
+
href?: string;
|
|
671
|
+
target?: string;
|
|
672
|
+
icon?: string;
|
|
673
|
+
leading?: ReactNode;
|
|
674
|
+
badge?: SidebarNavBadge;
|
|
675
|
+
disabled?: boolean;
|
|
676
|
+
danger?: boolean;
|
|
677
|
+
};
|
|
678
|
+
/**
|
|
679
|
+
* One level of nesting, the depth `sidebarNav()` renders: its `sideGroup()` maps
|
|
680
|
+
* children through `sideLeaf()`, which has no group branch. A child's own `items`
|
|
681
|
+
* are dropped here too, so the two halves render the same tree.
|
|
682
|
+
*/
|
|
683
|
+
type SidebarNavItem = SidebarNavLeaf & {
|
|
684
|
+
items?: SidebarNavLeaf[];
|
|
685
|
+
defaultOpen?: boolean;
|
|
686
|
+
};
|
|
687
|
+
type SidebarNavSection = {
|
|
688
|
+
label?: string;
|
|
689
|
+
items: SidebarNavItem[];
|
|
690
|
+
};
|
|
691
|
+
type SidebarNavProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
|
|
692
|
+
items?: SidebarNavItem[];
|
|
693
|
+
sections?: SidebarNavSection[];
|
|
694
|
+
active?: string;
|
|
695
|
+
activeIs?: 'page' | 'section';
|
|
696
|
+
collapsed?: boolean;
|
|
697
|
+
footer?: ReactNode;
|
|
698
|
+
/** Spread all link props onto the router link to retain its name and current state. */
|
|
699
|
+
renderLink?: (item: SidebarNavItem, props: ComponentPropsWithoutRef<'a'>) => ReactNode;
|
|
700
|
+
};
|
|
701
|
+
declare const SidebarNav: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLElement>, HTMLElement>, "ref">, "children"> & {
|
|
702
|
+
items?: SidebarNavItem[];
|
|
703
|
+
sections?: SidebarNavSection[];
|
|
704
|
+
active?: string;
|
|
705
|
+
activeIs?: "page" | "section";
|
|
706
|
+
collapsed?: boolean;
|
|
707
|
+
footer?: ReactNode;
|
|
708
|
+
/** Spread all link props onto the router link to retain its name and current state. */
|
|
709
|
+
renderLink?: (item: SidebarNavItem, props: ComponentPropsWithoutRef<"a">) => ReactNode;
|
|
710
|
+
} & react.RefAttributes<HTMLElement>>;
|
|
711
|
+
|
|
662
712
|
declare const PAGE_SIZES: readonly number[];
|
|
663
713
|
declare const DEFAULT_PAGE_SIZE: number;
|
|
664
714
|
|
|
665
|
-
export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, EmptyState, type EmptyStateProps, type EmptyStateVariant, Field, type FieldControlProps, type FieldProps, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, StatusDot, type StatusDotProps, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
|
|
715
|
+
export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, EmptyState, type EmptyStateProps, type EmptyStateVariant, Field, type FieldControlProps, type FieldProps, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, SidebarNav, type SidebarNavBadge, type SidebarNavItem, type SidebarNavLeaf, type SidebarNavProps, type SidebarNavSection, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, StatusDot, type StatusDotProps, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
|
package/react/dist/index.js
CHANGED
|
@@ -2452,9 +2452,108 @@ function Toast({ children }) {
|
|
|
2452
2452
|
}
|
|
2453
2453
|
|
|
2454
2454
|
// src/AppShell.tsx
|
|
2455
|
-
import { useEffect as
|
|
2455
|
+
import { useEffect as useEffect13, useId as useId12, useState as useState12 } from "react";
|
|
2456
2456
|
import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName } from "@apliteni/apliteni-ui";
|
|
2457
|
+
|
|
2458
|
+
// src/SidebarNav.tsx
|
|
2459
|
+
import { forwardRef as forwardRef5, useEffect as useEffect12, useId as useId11, useState as useState11 } from "react";
|
|
2460
|
+
import { ENTRANCE_FALLBACK_MS } from "@apliteni/apliteni-ui/motion";
|
|
2457
2461
|
import { Fragment as Fragment7, jsx as jsx28, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
2462
|
+
var badgeText = (badge) => typeof badge === "object" ? badge.text : badge;
|
|
2463
|
+
function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
|
|
2464
|
+
const uid = useId11();
|
|
2465
|
+
const group = !sub && !!item.items?.length;
|
|
2466
|
+
const childActive = group && item.items.some((child) => child.id === active);
|
|
2467
|
+
const [expanded, setExpanded] = useState11();
|
|
2468
|
+
const open = expanded ?? item.defaultOpen ?? childActive;
|
|
2469
|
+
const [entering, setEntering] = useState11(false);
|
|
2470
|
+
useEffect12(() => {
|
|
2471
|
+
if (!entering) return;
|
|
2472
|
+
const timer = setTimeout(() => setEntering(false), ENTRANCE_FALLBACK_MS);
|
|
2473
|
+
return () => clearTimeout(timer);
|
|
2474
|
+
}, [entering]);
|
|
2475
|
+
const count = badgeText(item.badge);
|
|
2476
|
+
const name = `${item.label}${count == null || count === "" ? "" : ` ${count}`}`;
|
|
2477
|
+
const isActive = !group && item.id === active;
|
|
2478
|
+
const className = [
|
|
2479
|
+
"ui-nav__item",
|
|
2480
|
+
sub && "ui-nav__item--sub",
|
|
2481
|
+
group && "ui-nav__toggle",
|
|
2482
|
+
isActive && "is-active",
|
|
2483
|
+
childActive && "is-current",
|
|
2484
|
+
item.disabled && "is-disabled",
|
|
2485
|
+
item.danger && "is-danger"
|
|
2486
|
+
].filter(Boolean).join(" ");
|
|
2487
|
+
const children = /* @__PURE__ */ jsxs23(Fragment7, { children: [
|
|
2488
|
+
(item.leading ?? item.icon) && /* @__PURE__ */ jsx28("span", { className: "ui-nav__ic", children: item.leading ?? /* @__PURE__ */ jsx28(Icon, { name: item.icon }) }),
|
|
2489
|
+
/* @__PURE__ */ jsx28("span", { className: "ui-nav__label", children: item.label }),
|
|
2490
|
+
count != null && count !== "" && /* @__PURE__ */ jsx28("span", { className: `ui-nav__badge is-${typeof item.badge === "object" ? item.badge.tone ?? "neutral" : "neutral"}`, children: count }),
|
|
2491
|
+
group && /* @__PURE__ */ jsx28("span", { className: "ui-nav__caret", "aria-hidden": "true" })
|
|
2492
|
+
] });
|
|
2493
|
+
const common = { className, "aria-label": name, title: collapsed ? name : void 0, children };
|
|
2494
|
+
if (group) return /* @__PURE__ */ jsxs23("li", { className: `ui-nav__group${open ? " is-open" : ""}`, children: [
|
|
2495
|
+
/* @__PURE__ */ jsx28(
|
|
2496
|
+
"button",
|
|
2497
|
+
{
|
|
2498
|
+
...common,
|
|
2499
|
+
type: "button",
|
|
2500
|
+
disabled: item.disabled,
|
|
2501
|
+
"aria-expanded": open,
|
|
2502
|
+
"aria-controls": uid,
|
|
2503
|
+
onClick: () => {
|
|
2504
|
+
setExpanded(!open);
|
|
2505
|
+
setEntering(!open);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
),
|
|
2509
|
+
/* @__PURE__ */ jsx28("ul", { id: uid, className: `ui-nav__sub${entering ? " is-entering" : ""}`, hidden: !open, onAnimationEnd: () => setEntering(false), children: item.items.map((child) => /* @__PURE__ */ jsx28(Row, { item: child, active, activeIs, collapsed, renderLink, sub: true }, child.id)) })
|
|
2510
|
+
] });
|
|
2511
|
+
if (item.disabled) return /* @__PURE__ */ jsx28("li", { children: /* @__PURE__ */ jsx28("span", { ...common, role: "link", "aria-disabled": "true" }) });
|
|
2512
|
+
const props = {
|
|
2513
|
+
...common,
|
|
2514
|
+
href: item.href ?? `#${item.id}`,
|
|
2515
|
+
target: item.target,
|
|
2516
|
+
"aria-current": isActive ? activeIs === "section" ? "true" : "page" : void 0
|
|
2517
|
+
};
|
|
2518
|
+
return /* @__PURE__ */ jsx28("li", { children: renderLink ? renderLink(item, props) : /* @__PURE__ */ jsx28("a", { ...props }) });
|
|
2519
|
+
}
|
|
2520
|
+
function Section({ section, ...props }) {
|
|
2521
|
+
const uid = useId11();
|
|
2522
|
+
return /* @__PURE__ */ jsxs23("div", { className: "ui-nav__section", children: [
|
|
2523
|
+
section.label && /* @__PURE__ */ jsx28("div", { className: "ui-nav__cap", id: uid, "aria-hidden": props.collapsed || void 0, children: section.label }),
|
|
2524
|
+
/* @__PURE__ */ jsx28("ul", { className: "ui-nav__list", "aria-labelledby": section.label ? uid : void 0, children: section.items.map((item) => /* @__PURE__ */ jsx28(Row, { item, ...props }, item.id)) })
|
|
2525
|
+
] });
|
|
2526
|
+
}
|
|
2527
|
+
var SidebarNav = forwardRef5(function SidebarNav2({
|
|
2528
|
+
items = [],
|
|
2529
|
+
sections,
|
|
2530
|
+
active,
|
|
2531
|
+
activeIs = "page",
|
|
2532
|
+
collapsed = false,
|
|
2533
|
+
footer,
|
|
2534
|
+
renderLink,
|
|
2535
|
+
className,
|
|
2536
|
+
"aria-label": label = "Sidebar",
|
|
2537
|
+
...rest
|
|
2538
|
+
}, ref) {
|
|
2539
|
+
return /* @__PURE__ */ jsxs23("nav", { ...rest, ref, "aria-label": label, className: ["ui-nav", "ui-nav--side", collapsed && "is-collapsed", className].filter(Boolean).join(" "), children: [
|
|
2540
|
+
(sections?.length ? sections : [{ items }]).map((section, index) => /* @__PURE__ */ jsx28(
|
|
2541
|
+
Section,
|
|
2542
|
+
{
|
|
2543
|
+
section,
|
|
2544
|
+
active,
|
|
2545
|
+
activeIs,
|
|
2546
|
+
collapsed,
|
|
2547
|
+
renderLink
|
|
2548
|
+
},
|
|
2549
|
+
index
|
|
2550
|
+
)),
|
|
2551
|
+
footer && /* @__PURE__ */ jsx28("div", { className: "ui-nav__foot", children: footer })
|
|
2552
|
+
] });
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
// src/AppShell.tsx
|
|
2556
|
+
import { Fragment as Fragment8, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
2458
2557
|
var path = (value) => value.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
|
|
2459
2558
|
function AppShell({
|
|
2460
2559
|
sections,
|
|
@@ -2475,21 +2574,21 @@ function AppShell({
|
|
|
2475
2574
|
palette,
|
|
2476
2575
|
renderLink
|
|
2477
2576
|
}) {
|
|
2478
|
-
const uid =
|
|
2479
|
-
const [collapsed, setCollapsed] =
|
|
2480
|
-
const [search, setSearch] =
|
|
2481
|
-
const [more, setMore] =
|
|
2482
|
-
const [theme, setTheme] =
|
|
2577
|
+
const uid = useId12();
|
|
2578
|
+
const [collapsed, setCollapsed] = useState12(defaultCollapsed);
|
|
2579
|
+
const [search, setSearch] = useState12(false);
|
|
2580
|
+
const [more, setMore] = useState12(false);
|
|
2581
|
+
const [theme, setTheme] = useState12("dark");
|
|
2483
2582
|
const current = path(pathname);
|
|
2484
2583
|
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];
|
|
2485
|
-
|
|
2584
|
+
useEffect13(() => {
|
|
2486
2585
|
setCollapsed(railCollapsed() ?? defaultCollapsed);
|
|
2487
2586
|
}, [defaultCollapsed]);
|
|
2488
|
-
|
|
2587
|
+
useEffect13(() => {
|
|
2489
2588
|
setMore(false);
|
|
2490
2589
|
setSearch(false);
|
|
2491
2590
|
}, [pathname]);
|
|
2492
|
-
|
|
2591
|
+
useEffect13(() => {
|
|
2493
2592
|
const root = document.documentElement;
|
|
2494
2593
|
const sync2 = () => setTheme(root.getAttribute("data-theme") === "light" ? "light" : "dark");
|
|
2495
2594
|
sync2();
|
|
@@ -2497,7 +2596,7 @@ function AppShell({
|
|
|
2497
2596
|
observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
|
|
2498
2597
|
return () => observer.disconnect();
|
|
2499
2598
|
}, []);
|
|
2500
|
-
|
|
2599
|
+
useEffect13(() => {
|
|
2501
2600
|
const hotkey = (event) => {
|
|
2502
2601
|
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "k") return;
|
|
2503
2602
|
const target = event.target;
|
|
@@ -2520,49 +2619,68 @@ function AppShell({
|
|
|
2520
2619
|
const linkProps = {
|
|
2521
2620
|
href: section.href,
|
|
2522
2621
|
className: `ui-nav__item${section === active ? " is-active" : ""}`,
|
|
2523
|
-
|
|
2622
|
+
// guidelines/going-back.md, section-lit: the same row, so the same reading as
|
|
2623
|
+
// the rail's. "page" here would announce the list as the page the reader is on.
|
|
2624
|
+
"aria-current": section === active ? back ? "true" : "page" : void 0,
|
|
2524
2625
|
"aria-label": `${section.label}${section.count == null ? "" : ` ${section.count}`}`,
|
|
2525
2626
|
onClick: (event) => {
|
|
2526
2627
|
if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
|
|
2527
2628
|
},
|
|
2528
|
-
children: /* @__PURE__ */
|
|
2529
|
-
/* @__PURE__ */
|
|
2530
|
-
/* @__PURE__ */
|
|
2531
|
-
section.count != null && /* @__PURE__ */
|
|
2629
|
+
children: /* @__PURE__ */ jsxs24(Fragment8, { children: [
|
|
2630
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx29(Icon, { name: section.icon }) }),
|
|
2631
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-nav__label", children: section.label }),
|
|
2632
|
+
section.count != null && /* @__PURE__ */ jsx29("span", { className: "ui-nav__badge is-neutral", children: section.count })
|
|
2532
2633
|
] })
|
|
2533
2634
|
};
|
|
2534
|
-
return /* @__PURE__ */
|
|
2635
|
+
return /* @__PURE__ */ jsx29("li", { children: renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx29("a", { ...linkProps }) }, section.href);
|
|
2535
2636
|
});
|
|
2637
|
+
const sidebar = (items, label) => /* @__PURE__ */ jsx29(
|
|
2638
|
+
SidebarNav,
|
|
2639
|
+
{
|
|
2640
|
+
"aria-label": label,
|
|
2641
|
+
items: items.map((section) => ({ ...section, id: section.href, badge: section.count })),
|
|
2642
|
+
active: active?.href,
|
|
2643
|
+
activeIs: back ? "section" : "page",
|
|
2644
|
+
renderLink: (item, props) => {
|
|
2645
|
+
const section = items.find((section2) => section2.href === item.id);
|
|
2646
|
+
const linkProps = { ...props, onClick: (event) => {
|
|
2647
|
+
if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
|
|
2648
|
+
} };
|
|
2649
|
+
return renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx29("a", { ...linkProps });
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
);
|
|
2536
2653
|
const foldLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
|
|
2537
2654
|
const visible = sections.length > 4 ? sections.slice(0, 3) : sections;
|
|
2538
2655
|
const remaining = sections.slice(visible.length);
|
|
2539
2656
|
const initials = account.name.trim().split(/\s+/).map((part) => part[0]).slice(0, 2).join("");
|
|
2540
|
-
return /* @__PURE__ */
|
|
2541
|
-
/* @__PURE__ */
|
|
2542
|
-
/* @__PURE__ */
|
|
2543
|
-
brand ?? /* @__PURE__ */
|
|
2544
|
-
/* @__PURE__ */
|
|
2657
|
+
return /* @__PURE__ */ jsxs24("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
|
|
2658
|
+
/* @__PURE__ */ jsxs24("div", { className: "ui-app__rail", children: [
|
|
2659
|
+
/* @__PURE__ */ jsx29("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs24("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
|
|
2660
|
+
brand ?? /* @__PURE__ */ jsx29("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
|
|
2661
|
+
/* @__PURE__ */ jsx29("span", { children: word })
|
|
2545
2662
|
] }) }),
|
|
2546
|
-
|
|
2547
|
-
/* @__PURE__ */
|
|
2548
|
-
/* @__PURE__ */
|
|
2549
|
-
/* @__PURE__ */
|
|
2550
|
-
/* @__PURE__ */
|
|
2663
|
+
sidebar(sections, "Sections"),
|
|
2664
|
+
/* @__PURE__ */ jsx29("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx29("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs24("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
|
|
2665
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs24("svg", { className: "ui-icon", viewBox: "0 0 24 24", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
2666
|
+
/* @__PURE__ */ jsx29("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
|
|
2667
|
+
/* @__PURE__ */ jsx29("path", { className: "ui-app__fold-seam", d: "M9 3v18" }),
|
|
2668
|
+
/* @__PURE__ */ jsx29("path", { className: "ui-app__fold-arrow", d: "m16 15-3-3 3-3" })
|
|
2551
2669
|
] }) }),
|
|
2552
|
-
/* @__PURE__ */
|
|
2670
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-nav__label", children: foldLabel })
|
|
2553
2671
|
] }) }) })
|
|
2554
2672
|
] }),
|
|
2555
|
-
/* @__PURE__ */
|
|
2556
|
-
/* @__PURE__ */
|
|
2557
|
-
/* @__PURE__ */
|
|
2558
|
-
/* @__PURE__ */
|
|
2559
|
-
/* @__PURE__ */
|
|
2560
|
-
/* @__PURE__ */
|
|
2673
|
+
/* @__PURE__ */ jsxs24("div", { className: "ui-app__well", children: [
|
|
2674
|
+
/* @__PURE__ */ jsxs24("header", { className: "ui-app__bar", children: [
|
|
2675
|
+
/* @__PURE__ */ jsxs24("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
|
|
2676
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx29(Icon, { name: "search" }) }),
|
|
2677
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-app__search-txt", children: "Search or run a command\u2026" }),
|
|
2678
|
+
/* @__PURE__ */ jsx29("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
|
|
2561
2679
|
] }),
|
|
2562
|
-
/* @__PURE__ */
|
|
2680
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-app__bar-gap" }),
|
|
2563
2681
|
bandControl,
|
|
2564
|
-
/* @__PURE__ */
|
|
2565
|
-
/* @__PURE__ */
|
|
2682
|
+
/* @__PURE__ */ jsx29("button", { type: "button", className: "toggle", "aria-label": themeName(theme), title: themeName(theme), onClick: () => applyTheme(theme === "light" ? "dark" : "light"), children: /* @__PURE__ */ jsx29("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: themeIcon(theme) } }) }),
|
|
2683
|
+
/* @__PURE__ */ jsx29("div", { className: "ui-app__user ui-app__user--bar", children: /* @__PURE__ */ jsx29(
|
|
2566
2684
|
Dropdown,
|
|
2567
2685
|
{
|
|
2568
2686
|
variant: "menu",
|
|
@@ -2571,27 +2689,27 @@ function AppShell({
|
|
|
2571
2689
|
ariaLabel: `Signed in as ${account.name}, ${account.email}`,
|
|
2572
2690
|
triggerClass: "ui-app__user-trigger",
|
|
2573
2691
|
panelClass: "ui-app__user-panel",
|
|
2574
|
-
triggerContent: /* @__PURE__ */
|
|
2575
|
-
header: /* @__PURE__ */
|
|
2576
|
-
/* @__PURE__ */
|
|
2577
|
-
/* @__PURE__ */
|
|
2692
|
+
triggerContent: /* @__PURE__ */ jsx29("span", { className: "ui-app__av", "aria-hidden": "true", children: initials }),
|
|
2693
|
+
header: /* @__PURE__ */ jsxs24("div", { className: "ui-dropdown__head", children: [
|
|
2694
|
+
/* @__PURE__ */ jsx29("b", { children: account.name }),
|
|
2695
|
+
/* @__PURE__ */ jsx29("span", { children: account.email })
|
|
2578
2696
|
] }),
|
|
2579
2697
|
items: [{ label: "Sign out", icon: "logout", danger: true }],
|
|
2580
2698
|
onSelect: onSignOut
|
|
2581
2699
|
}
|
|
2582
2700
|
) })
|
|
2583
2701
|
] }),
|
|
2584
|
-
/* @__PURE__ */
|
|
2585
|
-
back && /* @__PURE__ */
|
|
2586
|
-
/* @__PURE__ */
|
|
2587
|
-
lede && /* @__PURE__ */
|
|
2588
|
-
actions && /* @__PURE__ */
|
|
2589
|
-
/* @__PURE__ */
|
|
2702
|
+
/* @__PURE__ */ jsxs24("main", { className: `ui-app__main${width === "wide" ? " ui-app__main--wide" : ""}`, children: [
|
|
2703
|
+
back && /* @__PURE__ */ jsx29(BackLink, { ...back }),
|
|
2704
|
+
/* @__PURE__ */ jsx29("h1", { children: title }),
|
|
2705
|
+
lede && /* @__PURE__ */ jsx29("p", { className: "ui-app__sub", children: lede }),
|
|
2706
|
+
actions && /* @__PURE__ */ jsx29("div", { className: "ui-react-app__actions", children: actions }),
|
|
2707
|
+
/* @__PURE__ */ jsx29("div", { className: "ui-app__body", children })
|
|
2590
2708
|
] })
|
|
2591
2709
|
] }),
|
|
2592
|
-
/* @__PURE__ */
|
|
2710
|
+
/* @__PURE__ */ jsx29("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs24("ul", { className: "ui-nav__list", children: [
|
|
2593
2711
|
links(visible),
|
|
2594
|
-
remaining.length > 0 && /* @__PURE__ */
|
|
2712
|
+
remaining.length > 0 && /* @__PURE__ */ jsx29("li", { children: /* @__PURE__ */ jsxs24(
|
|
2595
2713
|
"button",
|
|
2596
2714
|
{
|
|
2597
2715
|
type: "button",
|
|
@@ -2600,20 +2718,20 @@ function AppShell({
|
|
|
2600
2718
|
"aria-expanded": more,
|
|
2601
2719
|
onClick: () => setMore(true),
|
|
2602
2720
|
children: [
|
|
2603
|
-
/* @__PURE__ */
|
|
2604
|
-
/* @__PURE__ */
|
|
2721
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx29(Icon, { name: "moreHorizontal" }) }),
|
|
2722
|
+
/* @__PURE__ */ jsx29("span", { className: "ui-nav__label", children: "More" })
|
|
2605
2723
|
]
|
|
2606
2724
|
}
|
|
2607
2725
|
) })
|
|
2608
2726
|
] }) }),
|
|
2609
|
-
/* @__PURE__ */
|
|
2610
|
-
/* @__PURE__ */
|
|
2727
|
+
/* @__PURE__ */ jsx29(Drawer, { open: more, onClose: () => setMore(false), title: "More sections", side: "bottom", children: sidebar(remaining, "More sections") }),
|
|
2728
|
+
/* @__PURE__ */ jsx29(CommandPalette, { ...palette ?? { groups: [{ items: sections.map((section) => ({ id: section.href, ...section })) }] }, open: search, onClose: () => setSearch(false) })
|
|
2611
2729
|
] });
|
|
2612
2730
|
}
|
|
2613
2731
|
|
|
2614
2732
|
// src/EmptyState.tsx
|
|
2615
2733
|
import { icon as kitIcon2, illo } from "@apliteni/apliteni-ui";
|
|
2616
|
-
import { jsx as
|
|
2734
|
+
import { jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
|
|
2617
2735
|
var cases = {
|
|
2618
2736
|
"first-run": { title: "Nothing needs you yet", sub: "New items will appear here when they need your attention.", icon: "check" },
|
|
2619
2737
|
"no-matches": { title: "No results in this view", sub: "Try another search or clear your filters.", icon: "search" },
|
|
@@ -2625,18 +2743,18 @@ function EmptyState({ variant = "first-run", title, sub, icon: icon6, art, actio
|
|
|
2625
2743
|
const heading = title ?? preset.title;
|
|
2626
2744
|
const guidance = sub ?? preset.sub;
|
|
2627
2745
|
const glyph = icon6 ?? preset.icon;
|
|
2628
|
-
return /* @__PURE__ */
|
|
2629
|
-
art ? typeof art === "string" ? /* @__PURE__ */
|
|
2630
|
-
heading && /* @__PURE__ */
|
|
2631
|
-
guidance && /* @__PURE__ */
|
|
2632
|
-
actions && /* @__PURE__ */
|
|
2746
|
+
return /* @__PURE__ */ jsxs25("div", { className: "ui-empty", children: [
|
|
2747
|
+
art ? typeof art === "string" ? /* @__PURE__ */ jsx30("div", { className: "ui-empty__art", dangerouslySetInnerHTML: { __html: /<svg/.test(art) ? art : illo(art) } }) : /* @__PURE__ */ jsx30("div", { className: "ui-empty__art", "aria-hidden": "true", children: art }) : glyph ? /* @__PURE__ */ jsx30("div", { className: "ui-empty__icon", dangerouslySetInnerHTML: { __html: kitIcon2(glyph) } }) : null,
|
|
2748
|
+
heading && /* @__PURE__ */ jsx30("div", { className: "ui-empty__title", children: heading }),
|
|
2749
|
+
guidance && /* @__PURE__ */ jsx30("div", { className: "ui-empty__sub", children: guidance }),
|
|
2750
|
+
actions && /* @__PURE__ */ jsx30("div", { className: "ui-empty__actions", children: actions })
|
|
2633
2751
|
] });
|
|
2634
2752
|
}
|
|
2635
2753
|
|
|
2636
2754
|
// src/ThemeToggle.tsx
|
|
2637
2755
|
import { useSyncExternalStore } from "react";
|
|
2638
2756
|
import { icon as icon5 } from "@apliteni/apliteni-ui";
|
|
2639
|
-
import { jsx as
|
|
2757
|
+
import { jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
2640
2758
|
var STORAGE_KEY = "apliteni-strategy-theme";
|
|
2641
2759
|
var CHOICE_EVENT = "apliteni-theme-choice";
|
|
2642
2760
|
var THEME_INIT_SCRIPT = `(()=>{let t;try{t=localStorage.getItem('apliteni-strategy-theme')}catch{}document.documentElement.setAttribute('data-theme',t==='dark'||t==='light'?t:window.matchMedia?.('(prefers-color-scheme: light)').matches?'light':'dark')})()`;
|
|
@@ -2694,10 +2812,10 @@ function ThemeToggle({ labelled = false }) {
|
|
|
2694
2812
|
}
|
|
2695
2813
|
window.dispatchEvent(new Event(CHOICE_EVENT));
|
|
2696
2814
|
};
|
|
2697
|
-
const button = /* @__PURE__ */
|
|
2698
|
-
return labelled ? /* @__PURE__ */
|
|
2815
|
+
const button = /* @__PURE__ */ jsx31("button", { type: "button", className: "toggle", "aria-label": name, title: name, onClick: toggle, children: /* @__PURE__ */ jsx31("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: icon5(theme === "auto" ? "monitor" : theme === "light" ? "sun" : "moon") } }) });
|
|
2816
|
+
return labelled ? /* @__PURE__ */ jsxs26("span", { className: "ui-theme-toggle", children: [
|
|
2699
2817
|
button,
|
|
2700
|
-
/* @__PURE__ */
|
|
2818
|
+
/* @__PURE__ */ jsxs26("span", { children: [
|
|
2701
2819
|
label,
|
|
2702
2820
|
" theme"
|
|
2703
2821
|
] })
|
|
@@ -2738,6 +2856,7 @@ export {
|
|
|
2738
2856
|
RowIdentity,
|
|
2739
2857
|
Segmented,
|
|
2740
2858
|
SelectField,
|
|
2859
|
+
SidebarNav,
|
|
2741
2860
|
Skeleton,
|
|
2742
2861
|
SkeletonTable,
|
|
2743
2862
|
Snippet,
|
package/src/components/shell.js
CHANGED
|
@@ -135,12 +135,17 @@ export function railCollapsed(cookies) {
|
|
|
135
135
|
// The name says what the press will do, and aria-expanded says what the rail is.
|
|
136
136
|
const railName = (collapsed) => (collapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
137
137
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
138
|
+
// Lucide `panel-left-close`, unmodified: a frame, the seam at 9, and a chevron
|
|
139
|
+
// pointing the way the press moves the rail's edge. The folded rail mirrors the
|
|
140
|
+
// chevron about 15 in layout.css, which lands it exactly on Lucide
|
|
141
|
+
// `panel-left-open` — so both states are a shipped glyph and neither is drawn by
|
|
142
|
+
// hand. The seam does not move: it IS the rail, and the compartment it cuts off
|
|
143
|
+
// stays on the side the rail is on. Only the nodes are written here — a child a
|
|
144
|
+
// stylesheet can reach — and they are spliced into icon()'s own wrapper rather
|
|
145
|
+
// than a copy of it. why: docs/specification.md#the-page-shell
|
|
142
146
|
const MARK = '<rect x="3" y="3" width="18" height="18" rx="2"/>'
|
|
143
|
-
+ '<path class="ui-app__fold-seam" d="M9 3v18"/>'
|
|
147
|
+
+ '<path class="ui-app__fold-seam" d="M9 3v18"/>'
|
|
148
|
+
+ '<path class="ui-app__fold-arrow" d="m16 15-3-3 3-3"/>';
|
|
144
149
|
const railMark = () => icon('').replace('></svg>', `>${MARK}</svg>`);
|
|
145
150
|
|
|
146
151
|
// The rail's own skin, outside the <nav>: folding the rail is not a place to go.
|
package/src/styles/layout.css
CHANGED
|
@@ -106,13 +106,14 @@
|
|
|
106
106
|
/* Which row is current is structural here, not chromatic: the resting glyphs
|
|
107
107
|
step down so the current one is the brightest whatever --accent is doing, and
|
|
108
108
|
the bar beside it carries the hue. A 3x17px bar on a 45x44 tile was the
|
|
109
|
-
smallest of three weak signals doing the most work, so it grows with the row.
|
|
109
|
+
smallest of three weak signals doing the most work, so it grows with the row.
|
|
110
|
+
The current glyph's own ink is nav.css's rule now, not a copy here: since #429
|
|
111
|
+
EVERY rail reads it that way, so the rail's copy was the same sentence in a
|
|
112
|
+
second file. What is left here is the rail's own resting step. */
|
|
110
113
|
/* Re-tuned at #295, which took the light rail down a step with the page. Below 720px
|
|
111
114
|
the glyph is the whole row, so it is held at text grade rather than at 1.4.11's bar —
|
|
112
|
-
stories/apps/shell-states.test.js:
|
|
115
|
+
stories/apps/shell-states.test.js:1491 `a resting glyph is dimmer but still legible`. */
|
|
113
116
|
.ui-app__rail .ui-nav__ic svg { opacity: 0.66; }
|
|
114
|
-
.ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
|
|
115
|
-
.ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
|
|
116
117
|
.ui-app__rail .ui-nav__item.is-active::before { width: 3px; height: 62%; }
|
|
117
118
|
|
|
118
119
|
/* A destructive row of the rail is a navigation row and rests like one; the danger
|
|
@@ -224,12 +225,27 @@
|
|
|
224
225
|
.ui-app .ui-app__fold { color: var(--text); }
|
|
225
226
|
.ui-app .ui-app__fold:hover,
|
|
226
227
|
.ui-app .ui-app__fold:focus-visible { color: var(--strong); }
|
|
227
|
-
/* The
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
the
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
/* The seam holds at 9 at both widths, because it is the rail: the compartment it
|
|
229
|
+
cuts off is the narrow one, on the side the rail is on. What travels is the
|
|
230
|
+
chevron, and it says what the press will do rather than repeating the state —
|
|
231
|
+
the same thing the control's own accessible name says. shell.js draws it at
|
|
232
|
+
13..16, Lucide `panel-left-close`; mirrored about 15, the centre of the
|
|
233
|
+
compartment it stands in, it lands at 14..17, which is Lucide
|
|
234
|
+
`panel-left-open`. So the two states are two Lucide glyphs and the CSS between
|
|
235
|
+
them is one reflection, not a hand-drawn position. `transform-origin` is
|
|
236
|
+
written out because a scale, unlike the translate this replaces, is read from
|
|
237
|
+
wherever the origin is — left to the initial 0 0 it would fold the mark
|
|
238
|
+
through the viewBox's left edge.
|
|
239
|
+
|
|
240
|
+
The width's own clock, not the words', so the mark and the closing edge arrive
|
|
241
|
+
together; no `!important`, so the reduced-motion net still takes it.
|
|
242
|
+
why: docs/specification.md#the-page-shell */
|
|
243
|
+
.ui-app__fold .ui-app__fold-arrow {
|
|
244
|
+
transform-box: view-box;
|
|
245
|
+
transform-origin: 15px 12px;
|
|
246
|
+
transition: transform var(--dur-med) var(--ease);
|
|
247
|
+
}
|
|
248
|
+
.ui-app.is-collapsed .ui-app__fold .ui-app__fold-arrow { transform: scaleX(-1); }
|
|
233
249
|
|
|
234
250
|
/* -- The collapsed rail --------------------------------------------------- */
|
|
235
251
|
/* Each rule here has its twin in the 720px block below; nothing lays a row out
|
|
@@ -257,6 +273,16 @@
|
|
|
257
273
|
:where(.ui-app:not(.ui-app--topbar).is-collapsed) .ui-app__brand { opacity: 0; visibility: hidden; pointer-events: none; }
|
|
258
274
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
|
|
259
275
|
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
|
|
276
|
+
/* The current row is ONE plate on the glyph column. Same rules as nav.css writes
|
|
277
|
+
for a standalone folded rail, and for the same reasons: the bar stands in the
|
|
278
|
+
row's own left padding, which on the strip is the rail's edge; the hairline is
|
|
279
|
+
drawn on a row that keeps the open column, so its right edge is off the strip;
|
|
280
|
+
and --ui-nav-strip is twice the glyph's centre, so a box that wide at the row's
|
|
281
|
+
edge is centred on the glyph. why: docs/specification.md#the-page-shell */
|
|
282
|
+
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active { isolation: isolate; background: none; }
|
|
283
|
+
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
|
|
284
|
+
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item.is-active::before { left: 0; width: var(--ui-nav-strip); height: 100%; border-radius: 10px; background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ z-index: -1; }
|
|
285
|
+
:where(.ui-app.is-collapsed) .ui-app__rail .ui-nav__item:hover:not(:focus-visible) { outline: none; }
|
|
260
286
|
/* The label IS the chip. `fixed` is the fallback placement, out of the rail's own
|
|
261
287
|
clip; the @supports branch below pins it to the row and is the only one that
|
|
262
288
|
survives a scroll. The toggle's selector is unscoped, because it is wordless at
|
|
@@ -452,6 +478,10 @@
|
|
|
452
478
|
.ui-app__brand span { opacity: 0; pointer-events: none; }
|
|
453
479
|
.ui-app__rail .ui-nav__sub { margin-inline-start: 0; padding-inline-start: 0; border-inline-start-width: 0; }
|
|
454
480
|
.ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
|
|
481
|
+
.ui-app__rail .ui-nav__item.is-active { isolation: isolate; background: none; }
|
|
482
|
+
.ui-app__rail .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
|
|
483
|
+
.ui-app__rail .ui-nav__item.is-active::before { left: 0; width: var(--ui-nav-strip); height: 100%; border-radius: 10px; background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ z-index: -1; }
|
|
484
|
+
.ui-app__rail .ui-nav__item:hover:not(:focus-visible) { outline: none; }
|
|
455
485
|
/* The one line of this block that is deliberately NOT the reader's fold: a finger
|
|
456
486
|
is the only pointer this width has, so a row and the account block are held to
|
|
457
487
|
44px. Neither floor can spread to the press, where growing the box is the one
|
package/src/styles/nav.css
CHANGED
|
@@ -93,8 +93,12 @@
|
|
|
93
93
|
|
|
94
94
|
.ui-nav__ic { display: inline-flex; flex: none; }
|
|
95
95
|
.ui-nav__ic svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 2.2; opacity: 0.72; }
|
|
96
|
+
/* The glyph brightens with its row and takes the row's ink, not the accent. One
|
|
97
|
+
accent signal per row: that is the marker above, which is the only thing on a
|
|
98
|
+
rail that means "here". A glyph in --accent beside it is the same sentence
|
|
99
|
+
said twice, and beside an accent counter it was said three times. See #429. */
|
|
96
100
|
.ui-nav__item.is-active .ui-nav__ic svg,
|
|
97
|
-
.ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke:
|
|
101
|
+
.ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
|
|
98
102
|
.ui-nav__label {
|
|
99
103
|
flex: 1 1 auto; min-width: 0;
|
|
100
104
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
@@ -146,16 +150,14 @@
|
|
|
146
150
|
.ui-nav__badge.is-live { background: var(--chip-success-fill); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--chip-success-ink); }
|
|
147
151
|
.ui-nav__badge.is-danger { background: var(--glow-pink); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ color: var(--pink); }
|
|
148
152
|
.ui-nav__item.is-active .ui-nav__badge.is-neutral { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); }
|
|
149
|
-
/* Same shape, one row up: on
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
4.08 in dark Nebula,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
See #157 and docs/specification.md#colour-and-contrast. */
|
|
158
|
-
.ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--accent); }
|
|
153
|
+
/* Same shape, one row up: on the current row the accent counter drops both the
|
|
154
|
+
wash and the accent and reads as the neutral one beside it. The row already
|
|
155
|
+
carries the one accent signal it is allowed — the marker — and the count is a
|
|
156
|
+
number, not a second answer to "where am I". It also retires the pair #157
|
|
157
|
+
recorded: accent ink on --surface fell under WCAG AA in four of the eight
|
|
158
|
+
theme x accent cells (4.08 in dark Nebula), and no alpha cleared it. Nothing
|
|
159
|
+
now paints that pair. See #429 and docs/specification.md#colour-and-contrast. */
|
|
160
|
+
.ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */ color: var(--strong); }
|
|
159
161
|
|
|
160
162
|
/* Collapsible group */
|
|
161
163
|
.ui-nav__group { display: flex; flex-direction: column; }
|
|
@@ -248,6 +250,43 @@
|
|
|
248
250
|
}
|
|
249
251
|
.ui-nav--side.is-collapsed .ui-nav__item:not(:has(.ui-nav__ic))::after { opacity: 0.62; }
|
|
250
252
|
|
|
253
|
+
/* The current row on a folded rail: ONE plate on the glyph column, and nothing
|
|
254
|
+
else. The open rail's marker is a 3px bar standing in the row's own left
|
|
255
|
+
padding; on the strip that padding is the rail's edge, so the bar reads as a
|
|
256
|
+
tick on the rail rather than a mark on a row — and a nested row puts it
|
|
257
|
+
--space-3 further out again, outside the rail altogether. The hairline cannot
|
|
258
|
+
close either: a row keeps the open column and the rail clips it, so its right
|
|
259
|
+
edge is drawn off the strip.
|
|
260
|
+
|
|
261
|
+
The plate is the strip. --ui-nav-strip is twice a row's own glyph centre, so a
|
|
262
|
+
box that wide standing at the row's edge is centred on the glyph, and the mark
|
|
263
|
+
lands where the reader is already looking. --surface-3 is the kit's quiet
|
|
264
|
+
non-text fill — a step off the rail in both themes, carrying a glyph and never
|
|
265
|
+
a word. The references mark current the same way and with no hue at all:
|
|
266
|
+
Cloudflare's docs rail paints a flat plate, and lessly-ui's rail is "a plate and
|
|
267
|
+
the weight step, no hue". Here the glyph's own step from 0.66 to full ink is the
|
|
268
|
+
second half of that pair. See #429.
|
|
269
|
+
|
|
270
|
+
`isolation` is what puts the plate behind the glyph: a positioned ::before paints
|
|
271
|
+
over its row's content, and a negative z-index with no stacking context of its
|
|
272
|
+
own would drop it behind the rail instead. */
|
|
273
|
+
.ui-nav--side.is-collapsed .ui-nav__item.is-active { isolation: isolate; background: none; }
|
|
274
|
+
/* At rest only — box-shadow carries the hairline AND the focus ring (WCAG 2.4.7). */
|
|
275
|
+
.ui-nav--side.is-collapsed .ui-nav__item.is-active:not(:focus-visible) { box-shadow: none; }
|
|
276
|
+
.ui-nav--side.is-collapsed .ui-nav__item.is-active::before {
|
|
277
|
+
left: 0;
|
|
278
|
+
width: var(--ui-nav-strip);
|
|
279
|
+
height: 100%;
|
|
280
|
+
border-radius: 10px;
|
|
281
|
+
background: var(--surface-3); /* ring-gap: inherit — decorative or text-only surface has no nested focus target. */
|
|
282
|
+
z-index: -1;
|
|
283
|
+
}
|
|
284
|
+
/* Hover's hairline is an outline on a row that keeps the open column, so on the
|
|
285
|
+
strip it too is drawn with no right edge. A folded rail answers a pointer with
|
|
286
|
+
the row's own ink and the name beside it, which is the whole readout at this
|
|
287
|
+
width. At rest only, so the focus ring is left standing. */
|
|
288
|
+
.ui-nav--side.is-collapsed .ui-nav__item:hover:not(:focus-visible) { outline: none; }
|
|
289
|
+
|
|
251
290
|
/* -- Horizontal tabs ----------------------------------------------------- */
|
|
252
291
|
.ui-nav--tabs {
|
|
253
292
|
display: flex;
|