@apliteni/apliteni-ui 0.125.0 → 0.126.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/guidelines/colour-and-theming.md +33 -21
- package/package.json +1 -1
- package/react/README.md +60 -0
- package/react/dist/index.d.ts +45 -1
- package/react/dist/index.js +102 -9
- package/src/components/index.js +1 -1
- package/src/components/nav.js +19 -4
- package/src/index.d.ts +8 -0
- package/src/index.js +1 -0
- package/src/logic/nav.js +65 -0
- package/src/styles/card.css +3 -0
- package/src/styles/nav.css +65 -4
|
@@ -4,53 +4,67 @@
|
|
|
4
4
|
|
|
5
5
|
**Rule:** Add and use a semantic colour token before writing a literal.
|
|
6
6
|
|
|
7
|
-
**Why:** Literals cannot follow
|
|
7
|
+
**Why:** Literals cannot follow themes or accents.
|
|
8
8
|
|
|
9
9
|
**Do:** Use tokens for ground and ink.
|
|
10
10
|
|
|
11
|
-
**Don't:** Freeze
|
|
11
|
+
**Don't:** Freeze dark-theme colours.
|
|
12
12
|
|
|
13
|
-
**Except:** Token definitions set
|
|
13
|
+
**Except:** Token definitions set colours. Mask `#000` controls alpha, not colour.
|
|
14
14
|
|
|
15
15
|
<!-- rule: tokens -->
|
|
16
16
|
|
|
17
17
|
## Stable signals
|
|
18
18
|
|
|
19
|
-
**Rule:**
|
|
19
|
+
**Rule:** Accents can follow sub-themes, but `--pink` and `--green` stay fixed.
|
|
20
20
|
|
|
21
|
-
**Why:** If danger followed
|
|
21
|
+
**Why:** If danger followed accents, revoke and go would look identical.
|
|
22
22
|
|
|
23
|
-
**Do:** Keep
|
|
23
|
+
**Do:** Keep errors `--pink` under every accent.
|
|
24
24
|
|
|
25
25
|
**Don't:** Point `--pink` at the accent.
|
|
26
26
|
|
|
27
|
-
**Except:** Across `data-theme`, signals change: light deepens `--pink` from `#e97ca5` to `#b63361`
|
|
27
|
+
**Except:** Across `data-theme`, signals change: light deepens `--pink` from `#e97ca5` to `#b63361` for legibility on white.
|
|
28
28
|
|
|
29
29
|
<!-- rule: signals -->
|
|
30
30
|
|
|
31
31
|
## Accent contrast
|
|
32
32
|
|
|
33
|
-
**Rule:** Use `--accent-strong` for
|
|
33
|
+
**Rule:** Use `--accent-strong` for accent ground and `--accent` for accent ink.
|
|
34
34
|
|
|
35
|
-
**Why:** `--accent` is
|
|
35
|
+
**Why:** `--accent` is canvas ink; darker `--accent-strong` lets white clear AA.
|
|
36
36
|
|
|
37
|
-
**Do:**
|
|
37
|
+
**Do:** Set white text on `--accent-strong`.
|
|
38
38
|
|
|
39
|
-
**Don't:** Use `--accent` as
|
|
39
|
+
**Don't:** Use `--accent` as ground.
|
|
40
40
|
|
|
41
|
-
**Except:**
|
|
41
|
+
**Except:** Only Nebula gives the two tokens different values. Artur chose `--accent` for dark selected Segmented pills on #614.
|
|
42
42
|
|
|
43
43
|
<!-- rule: accent-strong -->
|
|
44
44
|
|
|
45
|
+
## Limit selection accents
|
|
46
|
+
|
|
47
|
+
<!-- rule: selection-accent -->
|
|
48
|
+
|
|
49
|
+
**Rule:** Limit selected navigation to one accent signal: a Segmented pill's fill or an underline tab's bar.
|
|
50
|
+
|
|
51
|
+
**Why:** Two accent marks on one item imply two states.
|
|
52
|
+
|
|
53
|
+
**Do:** Use contrasting ink on Segmented pills and strong ink above underline bars. See Keep text off grey fills for SidebarNav.
|
|
54
|
+
|
|
55
|
+
**Don't:** Add a second accent mark, such as an accent counter, to a selected tab.
|
|
56
|
+
|
|
57
|
+
**Except:** NavTabs pills keep accent ink on soft fill. Fill alone is too faint to mark the route. Artur chose this on #429.
|
|
58
|
+
|
|
45
59
|
## Theme checks
|
|
46
60
|
|
|
47
|
-
**Rule:** Check
|
|
61
|
+
**Rule:** Check both themes and two accents before a PR.
|
|
48
62
|
|
|
49
|
-
**Why:** A pair
|
|
63
|
+
**Why:** A pair passing AA in dark can fail in light over a card wash.
|
|
50
64
|
|
|
51
65
|
**Do:** Test both themes and two accents.
|
|
52
66
|
|
|
53
|
-
**Don't:** Check only dark
|
|
67
|
+
**Don't:** Check only dark and one accent.
|
|
54
68
|
|
|
55
69
|
|
|
56
70
|
<!-- rule: both-themes -->
|
|
@@ -59,17 +73,15 @@
|
|
|
59
73
|
|
|
60
74
|
<!-- rule: text-surface -->
|
|
61
75
|
|
|
62
|
-
**Rule:**
|
|
76
|
+
**Rule:** Keep text on page or card surfaces, never grey fills.
|
|
63
77
|
|
|
64
|
-
**Why:** Grey
|
|
78
|
+
**Why:** Grey reading areas appear disabled.
|
|
65
79
|
|
|
66
|
-
**Do:**
|
|
80
|
+
**Do:** Give inputs edges. Reserve grey fills for non-text states, such as disabled tracks and dividers.
|
|
67
81
|
|
|
68
82
|
**Don't:** Put text on grey fields, chips, headers or footers.
|
|
69
83
|
|
|
70
|
-
**Except:** The
|
|
71
|
-
|
|
72
|
-
**Except:** A status chip carries a soft fill. Where its tone reports no verdict it takes a light tint of muted ink that leaves the panel in one direction per theme, so the neutral tones read as the same family as the coloured ones.
|
|
84
|
+
**Except:** The page shell's SidebarNav rail is the card surface. Its current row and label take a quiet fill one rung off the rail, without accent or edge. An outline means focus. Artur chose this in r34 of #475. Neutral status chips use a light muted ink tint that leaves the panel in one direction per theme. They read as one family with coloured chips.
|
|
73
85
|
|
|
74
86
|
## Mark selection in forced colours
|
|
75
87
|
|
package/package.json
CHANGED
package/react/README.md
CHANGED
|
@@ -910,3 +910,63 @@ instead of 32, with the same line drawn in the middle of it.
|
|
|
910
910
|
{ label: 'Closing', kind: 'total' },
|
|
911
911
|
]} />
|
|
912
912
|
```
|
|
913
|
+
|
|
914
|
+
## NavTabs and Breadcrumbs
|
|
915
|
+
|
|
916
|
+
`NavTabs` renders route links with the kit’s underline or pill presentation. Pass
|
|
917
|
+
unique item IDs and set `active` to the current route’s ID. The current link has
|
|
918
|
+
`aria-current="page"`; disabled items are text outside the Tab order. Optional
|
|
919
|
+
badges accept text, numbers (including zero), or `{ text, tone }`.
|
|
920
|
+
|
|
921
|
+
```tsx
|
|
922
|
+
<NavTabs aria-label="Finance views" active="payouts" items={[
|
|
923
|
+
{ id: 'summary', label: 'Summary', href: '/finance' },
|
|
924
|
+
{ id: 'payouts', label: 'Payouts', href: '/payouts', badge: 3 },
|
|
925
|
+
{ id: 'exports', label: 'Exports', disabled: true },
|
|
926
|
+
]} />
|
|
927
|
+
<Breadcrumbs items={[
|
|
928
|
+
{ label: 'Finance', href: '/finance' },
|
|
929
|
+
{ label: 'Payouts', href: '/payouts' },
|
|
930
|
+
{ label: 'PY-4821' },
|
|
931
|
+
]} />
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
Use `Tabs` for panels within a page. `NavTabs` keeps normal link keyboard behavior.
|
|
935
|
+
An omitted `href` falls back to `#` plus the item ID. `variant="pill"` selects the
|
|
936
|
+
existing pill styles. A badge of `null` renders no badge, so a count that is not in
|
|
937
|
+
yet costs nothing to pass.
|
|
938
|
+
|
|
939
|
+
Both components take `renderLink`, the prop `AppShell` already uses, so a router
|
|
940
|
+
handles the navigation instead of the browser reloading the document:
|
|
941
|
+
|
|
942
|
+
```tsx
|
|
943
|
+
<NavTabs active="payouts" items={items}
|
|
944
|
+
renderLink={(item, { href, children, ...props }) => (
|
|
945
|
+
<Link {...props} to={href!}>{children}</Link>
|
|
946
|
+
)} />
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Without it both render a plain `<a>`. The kit's class, destination, current state
|
|
950
|
+
and children all arrive in `linkProps`, so pass them through rather than rebuilding
|
|
951
|
+
them.
|
|
952
|
+
|
|
953
|
+
`Breadcrumbs` renders an ordered trail. Earlier items link only when they have an
|
|
954
|
+
`href`; the final item is always current-page text. Items accept a decorative kit
|
|
955
|
+
`icon` name. Both components accept native navigation attributes and a forwarded
|
|
956
|
+
root ref. Use `aria-label` to distinguish navigation landmarks. Link items accept
|
|
957
|
+
`target`. Script, data, and VBScript destinations fall back to `#`.
|
|
958
|
+
|
|
959
|
+
Tab rows scroll within their container when the links do not fit; a breadcrumb
|
|
960
|
+
trail wraps instead. A row whose links do fit scrolls nothing and clips nothing, so
|
|
961
|
+
a focus ring in it keeps its complete band. The current route is revealed after
|
|
962
|
+
mounting and route changes. Content and container changes update the fit state
|
|
963
|
+
without revealing the route again, so they preserve the reader's scroll position
|
|
964
|
+
where space permits. Keyboard focus stays put throughout.
|
|
965
|
+
When a link already fits in the row, it keeps its content width. The cap only
|
|
966
|
+
limits one link that is wider than the row.
|
|
967
|
+
|
|
968
|
+
A row also scrolls to a link that takes focus, far enough that the ring the link
|
|
969
|
+
draws clears the scrolling edge rather than being cut off by it. `NavTabs` does
|
|
970
|
+
that from `onFocus`, and still calls an `onFocus` you pass. A row whose links are
|
|
971
|
+
all disabled holds no Tab stop, so the browser makes the row itself one; it draws
|
|
972
|
+
the kit's inward ring there, never the browser's own outline.
|
package/react/dist/index.d.ts
CHANGED
|
@@ -1071,7 +1071,51 @@ declare const AccentPicker: react.ForwardRefExoticComponent<Omit<HTMLAttributes<
|
|
|
1071
1071
|
options?: readonly Accent[];
|
|
1072
1072
|
} & react.RefAttributes<HTMLDivElement>>;
|
|
1073
1073
|
|
|
1074
|
+
type NavTabBadge = string | number | {
|
|
1075
|
+
text: string | number;
|
|
1076
|
+
tone?: 'neutral' | 'accent' | 'live' | 'danger';
|
|
1077
|
+
};
|
|
1078
|
+
type NavTabItem = {
|
|
1079
|
+
id: string;
|
|
1080
|
+
label: string;
|
|
1081
|
+
href?: string;
|
|
1082
|
+
target?: string;
|
|
1083
|
+
disabled?: boolean;
|
|
1084
|
+
badge?: NavTabBadge | null;
|
|
1085
|
+
};
|
|
1086
|
+
type NavTabsProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
|
|
1087
|
+
items: readonly NavTabItem[];
|
|
1088
|
+
active?: string;
|
|
1089
|
+
variant?: 'underline' | 'pill';
|
|
1090
|
+
/** Spread linkProps onto the router link, including children. */
|
|
1091
|
+
renderLink?: (item: NavTabItem, linkProps: ComponentPropsWithoutRef<'a'>) => ReactNode;
|
|
1092
|
+
};
|
|
1093
|
+
declare const NavTabs: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLElement>, HTMLElement>, "ref">, "children"> & {
|
|
1094
|
+
items: readonly NavTabItem[];
|
|
1095
|
+
active?: string;
|
|
1096
|
+
variant?: "underline" | "pill";
|
|
1097
|
+
/** Spread linkProps onto the router link, including children. */
|
|
1098
|
+
renderLink?: (item: NavTabItem, linkProps: ComponentPropsWithoutRef<"a">) => ReactNode;
|
|
1099
|
+
} & react.RefAttributes<HTMLElement>>;
|
|
1100
|
+
|
|
1101
|
+
type BreadcrumbItem = {
|
|
1102
|
+
label: string;
|
|
1103
|
+
href?: string;
|
|
1104
|
+
target?: string;
|
|
1105
|
+
icon?: string;
|
|
1106
|
+
};
|
|
1107
|
+
type BreadcrumbsProps = Omit<ComponentPropsWithoutRef<'nav'>, 'children'> & {
|
|
1108
|
+
items: readonly BreadcrumbItem[];
|
|
1109
|
+
/** Spread linkProps onto the router link, including children. */
|
|
1110
|
+
renderLink?: (item: BreadcrumbItem, linkProps: ComponentPropsWithoutRef<'a'>) => ReactNode;
|
|
1111
|
+
};
|
|
1112
|
+
declare const Breadcrumbs: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLElement>, HTMLElement>, "ref">, "children"> & {
|
|
1113
|
+
items: readonly BreadcrumbItem[];
|
|
1114
|
+
/** Spread linkProps onto the router link, including children. */
|
|
1115
|
+
renderLink?: (item: BreadcrumbItem, linkProps: ComponentPropsWithoutRef<"a">) => ReactNode;
|
|
1116
|
+
} & react.RefAttributes<HTMLElement>>;
|
|
1117
|
+
|
|
1074
1118
|
declare const PAGE_SIZES: readonly number[];
|
|
1075
1119
|
declare const DEFAULT_PAGE_SIZE: number;
|
|
1076
1120
|
|
|
1077
|
-
export { type Accent, AccentPicker, type AccentPickerProps, type AddFilter, AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonLinkProps, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, Chart, type ChartBridgeStep, type ChartBridgeTones, type ChartPeriod, type ChartProps, type ChartSeries, type ChartTone, CheckList, type CheckListProps, Checkbox, type CheckboxProps, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DatePicker, type DatePickerMode, type DatePickerPreset, type DatePickerProps, type DatePickerRange, 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, FileDrop, type FileDropFile, type FileDropProps, type FileDropStatus, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, type NativeButtonProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, SearchField, type SearchFieldProps, 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, Success, SuccessCheck, type SuccessCheckProps, type SuccessCountdown, SuccessPanel, type SuccessPanelProps, type SuccessProps, Switch, type SwitchProps, 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 };
|
|
1121
|
+
export { type Accent, AccentPicker, type AccentPickerProps, type AddFilter, AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, type BreadcrumbItem, Breadcrumbs, type BreadcrumbsProps, BusyRegion, type BusyRegionProps, Button, type ButtonLinkProps, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, Chart, type ChartBridgeStep, type ChartBridgeTones, type ChartPeriod, type ChartProps, type ChartSeries, type ChartTone, CheckList, type CheckListProps, Checkbox, type CheckboxProps, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DatePicker, type DatePickerMode, type DatePickerPreset, type DatePickerProps, type DatePickerRange, 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, FileDrop, type FileDropFile, type FileDropProps, type FileDropStatus, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, type NativeButtonProps, type NavTabBadge, type NavTabItem, NavTabs, type NavTabsProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, Pill, type PillProps, RowIdentity, type RowIdentityProps, SearchField, type SearchFieldProps, 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, Success, SuccessCheck, type SuccessCheckProps, type SuccessCountdown, SuccessPanel, type SuccessPanelProps, type SuccessProps, Switch, type SwitchProps, 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
|
@@ -94,7 +94,7 @@ var cx = (...a) => a.filter(Boolean).join(" ");
|
|
|
94
94
|
var Button = forwardRef3(function Button2({
|
|
95
95
|
variant = "secondary",
|
|
96
96
|
size = "md",
|
|
97
|
-
icon:
|
|
97
|
+
icon: icon7,
|
|
98
98
|
leading,
|
|
99
99
|
iconRight,
|
|
100
100
|
iconOnly,
|
|
@@ -158,7 +158,7 @@ var Button = forwardRef3(function Button2({
|
|
|
158
158
|
className
|
|
159
159
|
);
|
|
160
160
|
const labelled = rest["aria-label"] != null || rest["aria-labelledby"] != null;
|
|
161
|
-
const fallback = typeof children === "string" && children.trim() ? children.trim() :
|
|
161
|
+
const fallback = typeof children === "string" && children.trim() ? children.trim() : icon7;
|
|
162
162
|
const named = iconOnly && !labelled && fallback ? { "aria-label": fallback, title: rest.title ?? fallback } : {};
|
|
163
163
|
const Root = href === void 0 ? "button" : "a";
|
|
164
164
|
return /* @__PURE__ */ jsxs3(
|
|
@@ -203,7 +203,7 @@ var Button = forwardRef3(function Button2({
|
|
|
203
203
|
if (!blockKey(event)) onKeyUp?.(event);
|
|
204
204
|
},
|
|
205
205
|
children: [
|
|
206
|
-
leading != null ? /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", style: { display: "inline-flex" }, children: leading }) :
|
|
206
|
+
leading != null ? /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", style: { display: "inline-flex" }, children: leading }) : icon7 && /* @__PURE__ */ jsx4(Icon, { name: icon7 }),
|
|
207
207
|
!iconOnly && renderedChildren != null && /* @__PURE__ */ jsx4("span", { className: "ui-btn__label-slot", children: /* @__PURE__ */ jsx4("span", { className: "ui-btn__label", ref: labelRef, children: renderedChildren }) }),
|
|
208
208
|
iconRight && /* @__PURE__ */ jsx4(Icon, { name: iconRight }),
|
|
209
209
|
busy && /* @__PURE__ */ jsxs3("span", { className: "ui-btn__dots", "aria-hidden": "true", children: [
|
|
@@ -273,9 +273,9 @@ function Card({ title, sub, level = 2, className, children }) {
|
|
|
273
273
|
// src/primitives/Callout.tsx
|
|
274
274
|
import { calloutIcons, icon as kitIcon } from "@apliteni/apliteni-ui";
|
|
275
275
|
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
276
|
-
function Callout({ variant, icon:
|
|
276
|
+
function Callout({ variant, icon: icon7 = calloutIcons[variant ?? "neutral"], children, actions }) {
|
|
277
277
|
return /* @__PURE__ */ jsxs5("div", { className: `ui-callout${variant && variant !== "neutral" ? ` ui-callout--${variant}` : ""}`, role: variant === "danger" ? "alert" : void 0, children: [
|
|
278
|
-
/* @__PURE__ */ jsx9("span", { className: "ui-callout__icon", dangerouslySetInnerHTML: { __html: kitIcon(
|
|
278
|
+
/* @__PURE__ */ jsx9("span", { className: "ui-callout__icon", dangerouslySetInnerHTML: { __html: kitIcon(icon7) } }),
|
|
279
279
|
/* @__PURE__ */ jsxs5("div", { className: "ui-callout__body", children: [
|
|
280
280
|
children,
|
|
281
281
|
actions != null && /* @__PURE__ */ jsx9("div", { className: "ui-toolbar ui-callout__actions", children: actions })
|
|
@@ -1581,12 +1581,12 @@ function Denied({
|
|
|
1581
1581
|
title = "You don\u2019t have access",
|
|
1582
1582
|
sub,
|
|
1583
1583
|
need,
|
|
1584
|
-
icon:
|
|
1584
|
+
icon: icon7 = "lock",
|
|
1585
1585
|
className,
|
|
1586
1586
|
children
|
|
1587
1587
|
}) {
|
|
1588
1588
|
return /* @__PURE__ */ jsxs14("div", { className: cx4("ui-denied", className), children: [
|
|
1589
|
-
/* @__PURE__ */ jsx18("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx18(Icon, { name:
|
|
1589
|
+
/* @__PURE__ */ jsx18("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx18(Icon, { name: icon7 }) }),
|
|
1590
1590
|
/* @__PURE__ */ jsx18("div", { className: "ui-denied__title", children: title }),
|
|
1591
1591
|
sub && /* @__PURE__ */ jsx18("div", { className: "ui-denied__sub", children: sub }),
|
|
1592
1592
|
need && /* @__PURE__ */ jsxs14("div", { className: "ui-denied__need", children: [
|
|
@@ -3903,11 +3903,11 @@ var cases = {
|
|
|
3903
3903
|
"not-found": { title: "We can't find that page", sub: "Check the address or return to the home page.", icon: "folder" },
|
|
3904
3904
|
"not-yet-built": { title: "This ships later", sub: "This screen is not available yet. Check back later.", icon: "clock" }
|
|
3905
3905
|
};
|
|
3906
|
-
function EmptyState({ variant = "first-run", title, sub, icon:
|
|
3906
|
+
function EmptyState({ variant = "first-run", title, sub, icon: icon7, art, actions }) {
|
|
3907
3907
|
const preset = cases[variant];
|
|
3908
3908
|
const heading = title ?? preset.title;
|
|
3909
3909
|
const guidance = sub ?? preset.sub;
|
|
3910
|
-
const glyph =
|
|
3910
|
+
const glyph = icon7 ?? preset.icon;
|
|
3911
3911
|
return /* @__PURE__ */ jsxs31("div", { className: "ui-empty", children: [
|
|
3912
3912
|
art ? typeof art === "string" ? /* @__PURE__ */ jsx36("div", { className: "ui-empty__art", dangerouslySetInnerHTML: { __html: /<svg/.test(art) ? art : illo(art) } }) : /* @__PURE__ */ jsx36("div", { className: "ui-empty__art", "aria-hidden": "true", children: art }) : glyph ? /* @__PURE__ */ jsx36("div", { className: "ui-empty__icon", dangerouslySetInnerHTML: { __html: kitIcon2(glyph) } }) : null,
|
|
3913
3913
|
heading && /* @__PURE__ */ jsx36("div", { className: "ui-empty__title", children: heading }),
|
|
@@ -4760,6 +4760,97 @@ var AccentPicker = forwardRef10(function AccentPicker2({
|
|
|
4760
4760
|
);
|
|
4761
4761
|
});
|
|
4762
4762
|
|
|
4763
|
+
// src/NavTabs.tsx
|
|
4764
|
+
import { Fragment as Fragment14, forwardRef as forwardRef11, useEffect as useEffect17, useRef as useRef21, useImperativeHandle as useImperativeHandle2 } from "react";
|
|
4765
|
+
import { safeUrl, observeNavFit, revealCurrentNav, revealFocusedNav } from "@apliteni/apliteni-ui";
|
|
4766
|
+
import { Fragment as Fragment15, jsx as jsx40, jsxs as jsxs34 } from "react/jsx-runtime";
|
|
4767
|
+
var NavTabs = forwardRef11(function NavTabs2({
|
|
4768
|
+
items,
|
|
4769
|
+
active,
|
|
4770
|
+
variant = "underline",
|
|
4771
|
+
renderLink,
|
|
4772
|
+
className,
|
|
4773
|
+
onFocus,
|
|
4774
|
+
"aria-label": ariaLabel = "Tabs",
|
|
4775
|
+
...rest
|
|
4776
|
+
}, ref) {
|
|
4777
|
+
const navRef = useRef21(null);
|
|
4778
|
+
useImperativeHandle2(ref, () => navRef.current, []);
|
|
4779
|
+
const signature = items.map((item) => item.id).join("\0");
|
|
4780
|
+
useEffect17(() => observeNavFit(navRef.current), []);
|
|
4781
|
+
useEffect17(() => {
|
|
4782
|
+
if (navRef.current) revealCurrentNav(navRef.current);
|
|
4783
|
+
}, [active, signature]);
|
|
4784
|
+
return /* @__PURE__ */ jsx40(
|
|
4785
|
+
"nav",
|
|
4786
|
+
{
|
|
4787
|
+
...rest,
|
|
4788
|
+
ref: navRef,
|
|
4789
|
+
className: ["ui-nav", "ui-nav--tabs", `is-${variant}`, className].filter(Boolean).join(" "),
|
|
4790
|
+
"aria-label": ariaLabel,
|
|
4791
|
+
onFocus: (event) => {
|
|
4792
|
+
if (navRef.current) revealFocusedNav(navRef.current, event.target);
|
|
4793
|
+
onFocus?.(event);
|
|
4794
|
+
},
|
|
4795
|
+
children: items.map((item) => {
|
|
4796
|
+
const badge = item.badge != null && typeof item.badge === "object" ? item.badge : { text: item.badge, tone: void 0 };
|
|
4797
|
+
const inner = /* @__PURE__ */ jsxs34(Fragment15, { children: [
|
|
4798
|
+
/* @__PURE__ */ jsx40("span", { className: "ui-nav__tab-label", children: item.label }),
|
|
4799
|
+
badge.text != null && badge.text !== "" && /* @__PURE__ */ jsx40("span", { className: `ui-nav__badge is-${badge.tone || "neutral"}`, children: badge.text })
|
|
4800
|
+
] });
|
|
4801
|
+
if (item.disabled) return /* @__PURE__ */ jsx40("span", { className: "ui-nav__tab is-disabled", "aria-disabled": "true", children: inner }, item.id);
|
|
4802
|
+
const current = item.id === active;
|
|
4803
|
+
const linkProps = {
|
|
4804
|
+
className: ["ui-nav__tab", current && "is-active"].filter(Boolean).join(" "),
|
|
4805
|
+
href: safeUrl(item.href || `#${item.id}`),
|
|
4806
|
+
target: item.target,
|
|
4807
|
+
"aria-current": current ? "page" : void 0,
|
|
4808
|
+
children: inner
|
|
4809
|
+
};
|
|
4810
|
+
return /* @__PURE__ */ jsx40(Fragment14, { children: renderLink ? renderLink(item, linkProps) : /* @__PURE__ */ jsx40("a", { ...linkProps }) }, item.id);
|
|
4811
|
+
})
|
|
4812
|
+
}
|
|
4813
|
+
);
|
|
4814
|
+
});
|
|
4815
|
+
|
|
4816
|
+
// src/Breadcrumbs.tsx
|
|
4817
|
+
import { forwardRef as forwardRef12 } from "react";
|
|
4818
|
+
import { icon as icon6, safeUrl as safeUrl2 } from "@apliteni/apliteni-ui";
|
|
4819
|
+
import { Fragment as Fragment16, jsx as jsx41, jsxs as jsxs35 } from "react/jsx-runtime";
|
|
4820
|
+
var Breadcrumbs = forwardRef12(function Breadcrumbs2({
|
|
4821
|
+
items,
|
|
4822
|
+
renderLink,
|
|
4823
|
+
className,
|
|
4824
|
+
"aria-label": ariaLabel = "Breadcrumb",
|
|
4825
|
+
...rest
|
|
4826
|
+
}, ref) {
|
|
4827
|
+
return /* @__PURE__ */ jsx41(
|
|
4828
|
+
"nav",
|
|
4829
|
+
{
|
|
4830
|
+
...rest,
|
|
4831
|
+
ref,
|
|
4832
|
+
className: ["ui-nav", "ui-nav--crumbs", className].filter(Boolean).join(" "),
|
|
4833
|
+
"aria-label": ariaLabel,
|
|
4834
|
+
children: /* @__PURE__ */ jsx41("ol", { className: "ui-nav__crumbs", children: items.map((item, index) => {
|
|
4835
|
+
const current = index === items.length - 1;
|
|
4836
|
+
const inner = /* @__PURE__ */ jsxs35(Fragment16, { children: [
|
|
4837
|
+
item.icon && /* @__PURE__ */ jsx41("span", { className: "ui-nav__ic", dangerouslySetInnerHTML: { __html: icon6(item.icon) } }),
|
|
4838
|
+
/* @__PURE__ */ jsx41("span", { className: "ui-nav__crumb-label", children: item.label })
|
|
4839
|
+
] });
|
|
4840
|
+
const linkProps = !current && item.href ? { className: "ui-nav__crumb", href: safeUrl2(item.href), target: item.target, children: inner } : null;
|
|
4841
|
+
return /* @__PURE__ */ jsx41("li", { className: "ui-nav__crumb-item", children: linkProps ? renderLink ? renderLink(item, linkProps) : /* @__PURE__ */ jsx41("a", { ...linkProps }) : /* @__PURE__ */ jsx41(
|
|
4842
|
+
"span",
|
|
4843
|
+
{
|
|
4844
|
+
className: ["ui-nav__crumb", current && "is-current"].filter(Boolean).join(" "),
|
|
4845
|
+
"aria-current": current ? "page" : void 0,
|
|
4846
|
+
children: inner
|
|
4847
|
+
}
|
|
4848
|
+
) }, index);
|
|
4849
|
+
}) })
|
|
4850
|
+
}
|
|
4851
|
+
);
|
|
4852
|
+
});
|
|
4853
|
+
|
|
4763
4854
|
// src/index.ts
|
|
4764
4855
|
var PAGE_SIZES = KIT_PAGE_SIZES;
|
|
4765
4856
|
var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
|
|
@@ -4768,6 +4859,7 @@ export {
|
|
|
4768
4859
|
AppShell,
|
|
4769
4860
|
BackLink,
|
|
4770
4861
|
Badge,
|
|
4862
|
+
Breadcrumbs,
|
|
4771
4863
|
BusyRegion,
|
|
4772
4864
|
Button,
|
|
4773
4865
|
Callout,
|
|
@@ -4793,6 +4885,7 @@ export {
|
|
|
4793
4885
|
Icon,
|
|
4794
4886
|
KeyValueList,
|
|
4795
4887
|
Modal,
|
|
4888
|
+
NavTabs,
|
|
4796
4889
|
NumericValue,
|
|
4797
4890
|
PAGE_SIZES,
|
|
4798
4891
|
Pagination,
|
package/src/components/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { icon } from '../assets/icons.js';
|
|
|
7
7
|
import { illo } from '../assets/illustrations.js';
|
|
8
8
|
import { successCheck } from './success.js';
|
|
9
9
|
import { esc, safeUrl } from '../html.js';
|
|
10
|
-
export { esc } from '../html.js';
|
|
10
|
+
export { esc, safeUrl } from '../html.js';
|
|
11
11
|
const cx = (...a) => a.filter(Boolean).join(' ');
|
|
12
12
|
// ---- Button --------------------------------------------------------------
|
|
13
13
|
// `iconSvg` is a raw leading-icon SVG string (trusted markup, not escaped) for
|
package/src/components/nav.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
//
|
|
6
6
|
// These are NAVIGATION controls, not tab panels — so tabs render as
|
|
7
7
|
// <nav> + <a aria-current="page">, not role="tablist" (that is what segmented()
|
|
8
|
-
// is for).
|
|
8
|
+
// is for). wireNav() opens sidebar groups and reveals the current route link.
|
|
9
9
|
import { esc, icon } from './index.js';
|
|
10
10
|
import { playEntrance } from '../motion.js';
|
|
11
11
|
import { safeUrl } from '../html.js';
|
|
12
|
+
import { observeNavFit, revealCurrentNav, revealFocusedNav } from '../logic/nav.js';
|
|
12
13
|
const cx = (...a) => a.filter(Boolean).join(' ');
|
|
13
14
|
|
|
14
15
|
// Unique-per-render ids so a section heading can label its own list. Module
|
|
@@ -177,9 +178,9 @@ export function nav({ variant = 'sidebar', ...opts } = {}) {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
// ---- Behaviour -----------------------------------------------------------
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
181
|
+
// Toggle a sidebar group's `.is-open` and its button's aria-expanded, and
|
|
182
|
+
// show/hide the nested list. Idempotent and event-delegated, so it's safe to
|
|
183
|
+
// call repeatedly (Storybook re-renders).
|
|
183
184
|
let _navGlobalWired = false;
|
|
184
185
|
|
|
185
186
|
function toggleGroup(btn) {
|
|
@@ -194,10 +195,24 @@ function toggleGroup(btn) {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
const fitObservers = new Map();
|
|
197
199
|
export function wireNav(root = document) {
|
|
200
|
+
for (const [nav, stop] of fitObservers) {
|
|
201
|
+
if (!nav.isConnected) { stop(); fitObservers.delete(nav); }
|
|
202
|
+
}
|
|
203
|
+
root.querySelectorAll('.ui-nav--tabs').forEach((nav) => {
|
|
204
|
+
if (!fitObservers.has(nav)) fitObservers.set(nav, observeNavFit(nav));
|
|
205
|
+
revealCurrentNav(nav);
|
|
206
|
+
});
|
|
198
207
|
// Per-collapsed-rail behaviour would go here; groups use one delegated handler.
|
|
199
208
|
if (_navGlobalWired) return;
|
|
200
209
|
_navGlobalWired = true;
|
|
210
|
+
// A tab row scrolls its own focused link clear of its edge, so the ring the link
|
|
211
|
+
// draws is not cut off by the scroll box. Delegated, because the links come and go.
|
|
212
|
+
document.addEventListener('focusin', (e) => {
|
|
213
|
+
const row = e.target.closest && e.target.closest('.ui-nav--tabs');
|
|
214
|
+
if (row) revealFocusedNav(row, e.target);
|
|
215
|
+
});
|
|
201
216
|
document.addEventListener('click', (e) => {
|
|
202
217
|
const btn = e.target.closest && e.target.closest('[data-nav-toggle]');
|
|
203
218
|
if (btn) { e.preventDefault(); toggleGroup(btn); }
|
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
/** Watch tab content and width without changing the current route. Call the result on removal. */
|
|
2
|
+
export declare function observeNavFit(nav: HTMLElement): () => void;
|
|
1
3
|
export declare function esc(value: unknown): string;
|
|
4
|
+
/** The URL boundary every factory and React component sends a destination through. */
|
|
5
|
+
export declare function safeUrl(value: unknown, fallback?: string): string;
|
|
6
|
+
/** Scroll a tab row so its `aria-current` link is inside the viewport. */
|
|
7
|
+
export declare function revealCurrentNav(nav: HTMLElement): void;
|
|
8
|
+
/** Scroll a tab row so the link a focus event landed on clears the row's edge. */
|
|
9
|
+
export declare function revealFocusedNav(nav: HTMLElement, target: EventTarget | null): void;
|
|
2
10
|
export declare function field(opts?: Record<string, unknown>): string;
|
|
3
11
|
export declare function input(opts?: Record<string, unknown>): string;
|
|
4
12
|
export declare function textarea(opts?: Record<string, unknown>): string;
|
package/src/index.js
CHANGED
|
@@ -35,4 +35,5 @@ export { PAGE_SIZES, DEFAULT_PAGE_SIZE } from './logic/pagination.js';
|
|
|
35
35
|
export { calloutIcons } from './logic/callout.js';
|
|
36
36
|
export { formatNumericValue, formatDeltaValue } from './logic/table-values.js';
|
|
37
37
|
export { accentSwatchStyle } from './logic/accents.js';
|
|
38
|
+
export { observeNavFit, revealCurrentNav, revealFocusedNav } from './logic/nav.js';
|
|
38
39
|
export { CHART_FLOOR, chartScale, bridgeWalk, pointTarget } from './logic/chart.js';
|
package/src/logic/nav.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// A tab row's padding is the room its links' focus rings are painted in, so an item
|
|
2
|
+
// brought to the row's CONTENT edge keeps its whole ring inside the scroll box: the
|
|
3
|
+
// solid band reaches --ring-gap-width + --ring-width = 3px out of a 4px padding.
|
|
4
|
+
// Both readers below want that edge — the current route so its label is not cut off,
|
|
5
|
+
// a link Tab landed on so its ring is not. Neither moves focus or scrolls the page;
|
|
6
|
+
// only the row's own scrollLeft changes.
|
|
7
|
+
function revealNavItem(nav, item) {
|
|
8
|
+
if (!item || nav.scrollWidth <= nav.clientWidth) return;
|
|
9
|
+
const bounds = nav.getBoundingClientRect();
|
|
10
|
+
const box = item.getBoundingClientRect();
|
|
11
|
+
const style = nav.ownerDocument.defaultView.getComputedStyle(nav);
|
|
12
|
+
const left = bounds.left + (parseFloat(style.paddingLeft) || 0);
|
|
13
|
+
const right = bounds.right - (parseFloat(style.paddingRight) || 0);
|
|
14
|
+
if (box.left < left) nav.scrollLeft += box.left - left;
|
|
15
|
+
else if (box.right > right) nav.scrollLeft += box.right - right;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Measure with overflow enabled so visible child paint cannot inflate scrollWidth.
|
|
19
|
+
// One pixel allows fractional columns. Unmeasured rows keep their scroll containment.
|
|
20
|
+
function fitNav(nav) {
|
|
21
|
+
nav.removeAttribute('data-nav-fit');
|
|
22
|
+
if (!nav.clientWidth) return;
|
|
23
|
+
if (nav.scrollWidth - nav.clientWidth <= 1) nav.setAttribute('data-nav-fit', '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Watch layout independently from route changes so a resize preserves the reader's scroll.
|
|
27
|
+
export function observeNavFit(nav) {
|
|
28
|
+
const win = nav.ownerDocument.defaultView;
|
|
29
|
+
const measure = () => fitNav(nav);
|
|
30
|
+
const resize = win.ResizeObserver ? new win.ResizeObserver(measure) : null;
|
|
31
|
+
const observe = () => {
|
|
32
|
+
resize?.disconnect();
|
|
33
|
+
resize?.observe(nav);
|
|
34
|
+
for (const child of nav.children) resize?.observe(child);
|
|
35
|
+
measure();
|
|
36
|
+
};
|
|
37
|
+
const changes = new win.MutationObserver(observe);
|
|
38
|
+
changes.observe(nav, {
|
|
39
|
+
childList: true, subtree: true, characterData: true,
|
|
40
|
+
attributes: true, attributeFilter: ['class', 'style'],
|
|
41
|
+
});
|
|
42
|
+
observe();
|
|
43
|
+
win.addEventListener('resize', measure);
|
|
44
|
+
return () => {
|
|
45
|
+
resize?.disconnect();
|
|
46
|
+
changes.disconnect();
|
|
47
|
+
win.removeEventListener('resize', measure);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Reveal only on an explicit route change or initial wiring.
|
|
52
|
+
export function revealCurrentNav(nav) {
|
|
53
|
+
fitNav(nav);
|
|
54
|
+
revealNavItem(nav, nav.querySelector('[aria-current="page"]'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Reveal the tab a focus event landed on. Chromium brings a focused child to the
|
|
58
|
+
// scroll box's own edge at best, which is the ring's room rather than the label's,
|
|
59
|
+
// so the solid band is clipped by the pixels the link hangs out: measured at 390px,
|
|
60
|
+
// 3.86px on the underline row and 13.86px on the pill row, with scrollLeft left at
|
|
61
|
+
// 0. Repaying the padding puts the band back inside. #429
|
|
62
|
+
export function revealFocusedNav(nav, target) {
|
|
63
|
+
const item = target && target.closest ? target.closest('.ui-nav__tab') : null;
|
|
64
|
+
if (item && nav.contains(item)) revealNavItem(nav, item);
|
|
65
|
+
}
|
package/src/styles/card.css
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
instead — a 22px penumbra off a 10px offset, the width Artur's reference uses on a
|
|
9
9
|
card. Chosen by him on #490 round t3. why: docs/foundations.md#elevation */
|
|
10
10
|
.ui-card {
|
|
11
|
+
--ui-card-ground: var(--surface); /* the ground a scrolling row inside covers its edge with; a tint repeats it below */
|
|
11
12
|
background: var(--surface);
|
|
12
13
|
border: 1px solid var(--card-edge);
|
|
13
14
|
box-shadow: var(--elev-rest);
|
|
@@ -51,10 +52,12 @@
|
|
|
51
52
|
works in dark sinks a light card below the page. Each variant edges itself in its own
|
|
52
53
|
colour. why: docs/foundations.md#elevation */
|
|
53
54
|
.ui-card--accent {
|
|
55
|
+
--ui-card-ground: color-mix(in srgb, var(--accent) var(--card-tint), var(--surface));
|
|
54
56
|
background: color-mix(in srgb, var(--accent) var(--card-tint), var(--surface));
|
|
55
57
|
border-color: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
56
58
|
}
|
|
57
59
|
.ui-card--live {
|
|
60
|
+
--ui-card-ground: color-mix(in srgb, var(--green) var(--card-tint), var(--surface));
|
|
58
61
|
background: color-mix(in srgb, var(--green) var(--card-tint), var(--surface));
|
|
59
62
|
border-color: color-mix(in srgb, var(--green) 22%, transparent);
|
|
60
63
|
}
|
package/src/styles/nav.css
CHANGED
|
@@ -285,14 +285,58 @@
|
|
|
285
285
|
|
|
286
286
|
/* -- Horizontal tabs ----------------------------------------------------- */
|
|
287
287
|
.ui-nav--tabs {
|
|
288
|
+
/* A scroll box clips at its padding edge, so the ring needs padding, not bare
|
|
289
|
+
paint room. Equal negative margins give the row back its place on the gutter
|
|
290
|
+
and in the flow, and the rule is painted as a background line one pixel above
|
|
291
|
+
the padding — a border-bottom would sit below it and lift the active
|
|
292
|
+
underline off the rule. */
|
|
293
|
+
--ui-tabs-ring-room: var(--space-1);
|
|
294
|
+
--ui-tabs-rule: 1px;
|
|
295
|
+
--ui-tabs-ground: var(--bg);
|
|
296
|
+
--ui-tabs-edge: color-mix(in srgb, var(--sheen) 12%, transparent);
|
|
297
|
+
min-width: 0;
|
|
298
|
+
max-width: calc(100% + 2 * var(--ui-tabs-ring-room));
|
|
299
|
+
overflow-x: auto;
|
|
300
|
+
padding: var(--ui-tabs-ring-room);
|
|
301
|
+
margin: calc(-1 * var(--ui-tabs-ring-room)) calc(-1 * var(--ui-tabs-ring-room))
|
|
302
|
+
calc(var(--ui-tabs-rule) - var(--ui-tabs-ring-room));
|
|
288
303
|
display: flex;
|
|
289
304
|
align-items: stretch;
|
|
290
305
|
gap: 4px;
|
|
291
|
-
|
|
306
|
+
/* Local covers hide the table-style shades when their edge is reached.
|
|
307
|
+
The row rule stays above the covers and keeps its original position. */
|
|
308
|
+
background-image:
|
|
309
|
+
linear-gradient(var(--border), var(--border)),
|
|
310
|
+
linear-gradient(to right, var(--ui-tabs-ground) 50%, transparent),
|
|
311
|
+
linear-gradient(to left, var(--ui-tabs-ground) 50%, transparent),
|
|
312
|
+
radial-gradient(farthest-side at 0 50%, var(--ui-tabs-edge), transparent),
|
|
313
|
+
radial-gradient(farthest-side at 100% 50%, var(--ui-tabs-edge), transparent);
|
|
314
|
+
background-repeat: no-repeat;
|
|
315
|
+
background-position:
|
|
316
|
+
center bottom calc(var(--ui-tabs-ring-room) - var(--ui-tabs-rule)),
|
|
317
|
+
0 0, 100% 0, 0 0, 100% 0;
|
|
318
|
+
background-size:
|
|
319
|
+
calc(100% - 2 * var(--ui-tabs-ring-room)) var(--ui-tabs-rule),
|
|
320
|
+
var(--space-6) 100%, var(--space-6) 100%, var(--space-3) 100%, var(--space-3) 100%;
|
|
321
|
+
background-attachment: scroll, local, local, scroll, scroll;
|
|
292
322
|
}
|
|
293
|
-
.
|
|
323
|
+
/* Fitting rows keep overflow visible. Measurements retain the same padding and margins,
|
|
324
|
+
so switching containment does not move the links. Unwired rows keep containment. */
|
|
325
|
+
.ui-nav--tabs[data-nav-fit] { overflow: visible; }
|
|
326
|
+
.ui-nav--tabs.is-pill { --ui-tabs-rule: 0px; gap: 6px; }
|
|
327
|
+
.ui-card .ui-nav--tabs { --ui-tabs-ground: var(--ui-card-ground); }
|
|
328
|
+
:root[data-theme="light"] .ui-nav--tabs {
|
|
329
|
+
--ui-tabs-edge: color-mix(in srgb, var(--shadow-ink) 13%, transparent);
|
|
330
|
+
}
|
|
331
|
+
/* The row is a keyboard stop of its own only while it scrolls AND holds no focusable
|
|
332
|
+
link — every tab disabled — and Chromium answers that stop with its black outline. It
|
|
333
|
+
is a scroll region, so it takes the kit's inward band like the other six: an outset
|
|
334
|
+
ring would be clipped by the very box that scrolls. #531's reading, on #429's row. */
|
|
335
|
+
.ui-nav--tabs:focus-visible { outline: var(--ring-scroll); outline-offset: var(--ring-scroll-offset); }
|
|
294
336
|
.ui-nav__tab {
|
|
337
|
+
flex: none;
|
|
295
338
|
position: relative;
|
|
339
|
+
max-width: 100%;
|
|
296
340
|
display: inline-flex;
|
|
297
341
|
align-items: center;
|
|
298
342
|
gap: 8px;
|
|
@@ -307,6 +351,7 @@
|
|
|
307
351
|
.ui-nav__tab:hover { color: var(--strong); }
|
|
308
352
|
.ui-nav__tab:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); border-radius: 8px; }
|
|
309
353
|
.ui-nav__tab.is-active { color: var(--strong); }
|
|
354
|
+
.ui-nav__tab-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
310
355
|
|
|
311
356
|
/* underline variant */
|
|
312
357
|
.ui-nav--tabs.is-underline .ui-nav__tab::after {
|
|
@@ -324,10 +369,26 @@
|
|
|
324
369
|
|
|
325
370
|
/* pill variant */
|
|
326
371
|
.ui-nav--tabs.is-pill .ui-nav__tab { border-radius: var(--radius-pill); padding: 8px 15px; }
|
|
327
|
-
.ui-nav--tabs.is-pill .ui-nav__tab:hover { background: var(--surface); /* code-bg: inherit — control paint;
|
|
372
|
+
.ui-nav--tabs.is-pill .ui-nav__tab:hover { background: var(--surface); /* code-bg: inherit — control paint; its focus indicator uses the containing surface. */ }
|
|
373
|
+
/* The current route keeps its quiet fill and uses accent ink. */
|
|
328
374
|
.ui-nav--tabs.is-pill .ui-nav__tab.is-active {
|
|
329
|
-
background: var(--glow-purple); /* code-bg: inherit — a wash paints over a ground and behind a chip, so the opaque ground below it is the step. */
|
|
330
375
|
color: var(--accent);
|
|
376
|
+
background: var(--glow-purple); /* code-bg: inherit — translucent wash retains the opaque containing gap. */
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Forced colours: the selected-tab marks are in the block below. */
|
|
380
|
+
@media (forced-colors: active) {
|
|
381
|
+
/* Forced colours drop background images, so the row's rule comes back as a border.
|
|
382
|
+
The last pixel of the bottom padding pays for it and the row keeps its size. The
|
|
383
|
+
border image leaves the ring room at both ends bare, so the rule ends on the gutter
|
|
384
|
+
as the background line does. */
|
|
385
|
+
.ui-nav--tabs {
|
|
386
|
+
padding-bottom: calc(var(--ui-tabs-ring-room) - var(--ui-tabs-rule));
|
|
387
|
+
border-bottom: var(--ui-tabs-rule) solid;
|
|
388
|
+
border-image: linear-gradient(to right, transparent var(--ui-tabs-ring-room),
|
|
389
|
+
CanvasText var(--ui-tabs-ring-room) calc(100% - var(--ui-tabs-ring-room)),
|
|
390
|
+
transparent calc(100% - var(--ui-tabs-ring-room))) 0 0 1 0;
|
|
391
|
+
}
|
|
331
392
|
}
|
|
332
393
|
|
|
333
394
|
.ui-nav__tab.is-disabled { /* muted-ink: state — unavailable control */ color: var(--disabled-ink); cursor: default; }
|