@apliteni/apliteni-ui 0.125.1 → 0.127.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 +60 -0
- package/react/dist/index.css +9 -0
- package/react/dist/index.d.ts +45 -1
- package/react/dist/index.js +106 -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/input.css +64 -9
- package/src/styles/nav.css +65 -4
- package/src/styles/table.css +3 -0
- package/src/styles/tap-zone.css +7 -0
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.css
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
height: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y)));
|
|
50
50
|
}
|
|
51
51
|
.ui-check input::before,
|
|
52
|
+
.ui-table__selection input[type=checkbox]::before,
|
|
52
53
|
.ui-toast__close::before {
|
|
53
54
|
width: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x))));
|
|
54
55
|
height: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y))));
|
|
@@ -102,6 +103,14 @@
|
|
|
102
103
|
--tap-clear-x: var(--tap-gap);
|
|
103
104
|
--tap-clear-y: 0px;
|
|
104
105
|
}
|
|
106
|
+
.ui-table__selection {
|
|
107
|
+
--tap-clear-x: var(--tap-min);
|
|
108
|
+
--tap-clear-y: var(--tap-min);
|
|
109
|
+
}
|
|
110
|
+
.ui-table--dense .ui-table__selection,
|
|
111
|
+
.ui-table--compact .ui-table__selection {
|
|
112
|
+
--tap-clear-y: 0px;
|
|
113
|
+
}
|
|
105
114
|
.ui-toast {
|
|
106
115
|
row-gap: var(--tap-gap) !important;
|
|
107
116
|
--tap-clear-y: var(--tap-gap);
|
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 })
|
|
@@ -1420,6 +1420,7 @@ function DataTable({
|
|
|
1420
1420
|
}
|
|
1421
1421
|
);
|
|
1422
1422
|
const pageAllOn = selectable && slice.length > 0 && slice.every((r) => selected.has(r.name));
|
|
1423
|
+
const pageSomeOn = selectable && !pageAllOn && slice.some((r) => selected.has(r.name));
|
|
1423
1424
|
return /* @__PURE__ */ jsxs13(Fragment4, { children: [
|
|
1424
1425
|
/* @__PURE__ */ jsx17(
|
|
1425
1426
|
"div",
|
|
@@ -1442,6 +1443,9 @@ function DataTable({
|
|
|
1442
1443
|
type: "checkbox",
|
|
1443
1444
|
checked: pageAllOn,
|
|
1444
1445
|
"aria-label": "Select all rows on this page",
|
|
1446
|
+
ref: (input) => {
|
|
1447
|
+
if (input) input.indeterminate = pageSomeOn;
|
|
1448
|
+
},
|
|
1445
1449
|
onChange: () => onTogglePage(slice.map((r) => r.name))
|
|
1446
1450
|
}
|
|
1447
1451
|
) }) : null,
|
|
@@ -1581,12 +1585,12 @@ function Denied({
|
|
|
1581
1585
|
title = "You don\u2019t have access",
|
|
1582
1586
|
sub,
|
|
1583
1587
|
need,
|
|
1584
|
-
icon:
|
|
1588
|
+
icon: icon7 = "lock",
|
|
1585
1589
|
className,
|
|
1586
1590
|
children
|
|
1587
1591
|
}) {
|
|
1588
1592
|
return /* @__PURE__ */ jsxs14("div", { className: cx4("ui-denied", className), children: [
|
|
1589
|
-
/* @__PURE__ */ jsx18("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx18(Icon, { name:
|
|
1593
|
+
/* @__PURE__ */ jsx18("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx18(Icon, { name: icon7 }) }),
|
|
1590
1594
|
/* @__PURE__ */ jsx18("div", { className: "ui-denied__title", children: title }),
|
|
1591
1595
|
sub && /* @__PURE__ */ jsx18("div", { className: "ui-denied__sub", children: sub }),
|
|
1592
1596
|
need && /* @__PURE__ */ jsxs14("div", { className: "ui-denied__need", children: [
|
|
@@ -3903,11 +3907,11 @@ var cases = {
|
|
|
3903
3907
|
"not-found": { title: "We can't find that page", sub: "Check the address or return to the home page.", icon: "folder" },
|
|
3904
3908
|
"not-yet-built": { title: "This ships later", sub: "This screen is not available yet. Check back later.", icon: "clock" }
|
|
3905
3909
|
};
|
|
3906
|
-
function EmptyState({ variant = "first-run", title, sub, icon:
|
|
3910
|
+
function EmptyState({ variant = "first-run", title, sub, icon: icon7, art, actions }) {
|
|
3907
3911
|
const preset = cases[variant];
|
|
3908
3912
|
const heading = title ?? preset.title;
|
|
3909
3913
|
const guidance = sub ?? preset.sub;
|
|
3910
|
-
const glyph =
|
|
3914
|
+
const glyph = icon7 ?? preset.icon;
|
|
3911
3915
|
return /* @__PURE__ */ jsxs31("div", { className: "ui-empty", children: [
|
|
3912
3916
|
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
3917
|
heading && /* @__PURE__ */ jsx36("div", { className: "ui-empty__title", children: heading }),
|
|
@@ -4760,6 +4764,97 @@ var AccentPicker = forwardRef10(function AccentPicker2({
|
|
|
4760
4764
|
);
|
|
4761
4765
|
});
|
|
4762
4766
|
|
|
4767
|
+
// src/NavTabs.tsx
|
|
4768
|
+
import { Fragment as Fragment14, forwardRef as forwardRef11, useEffect as useEffect17, useRef as useRef21, useImperativeHandle as useImperativeHandle2 } from "react";
|
|
4769
|
+
import { safeUrl, observeNavFit, revealCurrentNav, revealFocusedNav } from "@apliteni/apliteni-ui";
|
|
4770
|
+
import { Fragment as Fragment15, jsx as jsx40, jsxs as jsxs34 } from "react/jsx-runtime";
|
|
4771
|
+
var NavTabs = forwardRef11(function NavTabs2({
|
|
4772
|
+
items,
|
|
4773
|
+
active,
|
|
4774
|
+
variant = "underline",
|
|
4775
|
+
renderLink,
|
|
4776
|
+
className,
|
|
4777
|
+
onFocus,
|
|
4778
|
+
"aria-label": ariaLabel = "Tabs",
|
|
4779
|
+
...rest
|
|
4780
|
+
}, ref) {
|
|
4781
|
+
const navRef = useRef21(null);
|
|
4782
|
+
useImperativeHandle2(ref, () => navRef.current, []);
|
|
4783
|
+
const signature = items.map((item) => item.id).join("\0");
|
|
4784
|
+
useEffect17(() => observeNavFit(navRef.current), []);
|
|
4785
|
+
useEffect17(() => {
|
|
4786
|
+
if (navRef.current) revealCurrentNav(navRef.current);
|
|
4787
|
+
}, [active, signature]);
|
|
4788
|
+
return /* @__PURE__ */ jsx40(
|
|
4789
|
+
"nav",
|
|
4790
|
+
{
|
|
4791
|
+
...rest,
|
|
4792
|
+
ref: navRef,
|
|
4793
|
+
className: ["ui-nav", "ui-nav--tabs", `is-${variant}`, className].filter(Boolean).join(" "),
|
|
4794
|
+
"aria-label": ariaLabel,
|
|
4795
|
+
onFocus: (event) => {
|
|
4796
|
+
if (navRef.current) revealFocusedNav(navRef.current, event.target);
|
|
4797
|
+
onFocus?.(event);
|
|
4798
|
+
},
|
|
4799
|
+
children: items.map((item) => {
|
|
4800
|
+
const badge = item.badge != null && typeof item.badge === "object" ? item.badge : { text: item.badge, tone: void 0 };
|
|
4801
|
+
const inner = /* @__PURE__ */ jsxs34(Fragment15, { children: [
|
|
4802
|
+
/* @__PURE__ */ jsx40("span", { className: "ui-nav__tab-label", children: item.label }),
|
|
4803
|
+
badge.text != null && badge.text !== "" && /* @__PURE__ */ jsx40("span", { className: `ui-nav__badge is-${badge.tone || "neutral"}`, children: badge.text })
|
|
4804
|
+
] });
|
|
4805
|
+
if (item.disabled) return /* @__PURE__ */ jsx40("span", { className: "ui-nav__tab is-disabled", "aria-disabled": "true", children: inner }, item.id);
|
|
4806
|
+
const current = item.id === active;
|
|
4807
|
+
const linkProps = {
|
|
4808
|
+
className: ["ui-nav__tab", current && "is-active"].filter(Boolean).join(" "),
|
|
4809
|
+
href: safeUrl(item.href || `#${item.id}`),
|
|
4810
|
+
target: item.target,
|
|
4811
|
+
"aria-current": current ? "page" : void 0,
|
|
4812
|
+
children: inner
|
|
4813
|
+
};
|
|
4814
|
+
return /* @__PURE__ */ jsx40(Fragment14, { children: renderLink ? renderLink(item, linkProps) : /* @__PURE__ */ jsx40("a", { ...linkProps }) }, item.id);
|
|
4815
|
+
})
|
|
4816
|
+
}
|
|
4817
|
+
);
|
|
4818
|
+
});
|
|
4819
|
+
|
|
4820
|
+
// src/Breadcrumbs.tsx
|
|
4821
|
+
import { forwardRef as forwardRef12 } from "react";
|
|
4822
|
+
import { icon as icon6, safeUrl as safeUrl2 } from "@apliteni/apliteni-ui";
|
|
4823
|
+
import { Fragment as Fragment16, jsx as jsx41, jsxs as jsxs35 } from "react/jsx-runtime";
|
|
4824
|
+
var Breadcrumbs = forwardRef12(function Breadcrumbs2({
|
|
4825
|
+
items,
|
|
4826
|
+
renderLink,
|
|
4827
|
+
className,
|
|
4828
|
+
"aria-label": ariaLabel = "Breadcrumb",
|
|
4829
|
+
...rest
|
|
4830
|
+
}, ref) {
|
|
4831
|
+
return /* @__PURE__ */ jsx41(
|
|
4832
|
+
"nav",
|
|
4833
|
+
{
|
|
4834
|
+
...rest,
|
|
4835
|
+
ref,
|
|
4836
|
+
className: ["ui-nav", "ui-nav--crumbs", className].filter(Boolean).join(" "),
|
|
4837
|
+
"aria-label": ariaLabel,
|
|
4838
|
+
children: /* @__PURE__ */ jsx41("ol", { className: "ui-nav__crumbs", children: items.map((item, index) => {
|
|
4839
|
+
const current = index === items.length - 1;
|
|
4840
|
+
const inner = /* @__PURE__ */ jsxs35(Fragment16, { children: [
|
|
4841
|
+
item.icon && /* @__PURE__ */ jsx41("span", { className: "ui-nav__ic", dangerouslySetInnerHTML: { __html: icon6(item.icon) } }),
|
|
4842
|
+
/* @__PURE__ */ jsx41("span", { className: "ui-nav__crumb-label", children: item.label })
|
|
4843
|
+
] });
|
|
4844
|
+
const linkProps = !current && item.href ? { className: "ui-nav__crumb", href: safeUrl2(item.href), target: item.target, children: inner } : null;
|
|
4845
|
+
return /* @__PURE__ */ jsx41("li", { className: "ui-nav__crumb-item", children: linkProps ? renderLink ? renderLink(item, linkProps) : /* @__PURE__ */ jsx41("a", { ...linkProps }) : /* @__PURE__ */ jsx41(
|
|
4846
|
+
"span",
|
|
4847
|
+
{
|
|
4848
|
+
className: ["ui-nav__crumb", current && "is-current"].filter(Boolean).join(" "),
|
|
4849
|
+
"aria-current": current ? "page" : void 0,
|
|
4850
|
+
children: inner
|
|
4851
|
+
}
|
|
4852
|
+
) }, index);
|
|
4853
|
+
}) })
|
|
4854
|
+
}
|
|
4855
|
+
);
|
|
4856
|
+
});
|
|
4857
|
+
|
|
4763
4858
|
// src/index.ts
|
|
4764
4859
|
var PAGE_SIZES = KIT_PAGE_SIZES;
|
|
4765
4860
|
var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
|
|
@@ -4768,6 +4863,7 @@ export {
|
|
|
4768
4863
|
AppShell,
|
|
4769
4864
|
BackLink,
|
|
4770
4865
|
Badge,
|
|
4866
|
+
Breadcrumbs,
|
|
4771
4867
|
BusyRegion,
|
|
4772
4868
|
Button,
|
|
4773
4869
|
Callout,
|
|
@@ -4793,6 +4889,7 @@ export {
|
|
|
4793
4889
|
Icon,
|
|
4794
4890
|
KeyValueList,
|
|
4795
4891
|
Modal,
|
|
4892
|
+
NavTabs,
|
|
4796
4893
|
NumericValue,
|
|
4797
4894
|
PAGE_SIZES,
|
|
4798
4895
|
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/input.css
CHANGED
|
@@ -123,7 +123,9 @@
|
|
|
123
123
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235c6270' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/* Checkbox / radio — custom, token-driven
|
|
126
|
+
/* Checkbox / radio — custom, token-driven. DataTable's selection boxes are bare
|
|
127
|
+
inputs in a cell with no label, and take the same box so a partial page reads in
|
|
128
|
+
the kit's paint rather than the browser's. #619 */
|
|
127
129
|
.ui-check {
|
|
128
130
|
display: inline-flex;
|
|
129
131
|
align-items: flex-start;
|
|
@@ -133,7 +135,8 @@
|
|
|
133
135
|
color: var(--text);
|
|
134
136
|
line-height: 1.5;
|
|
135
137
|
}
|
|
136
|
-
.ui-check input
|
|
138
|
+
.ui-check input,
|
|
139
|
+
.ui-table__selection input[type="checkbox"] {
|
|
137
140
|
appearance: none;
|
|
138
141
|
width: 19px;
|
|
139
142
|
height: 19px;
|
|
@@ -146,6 +149,9 @@
|
|
|
146
149
|
transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
|
|
147
150
|
position: relative;
|
|
148
151
|
}
|
|
152
|
+
/* In a cell there is no label line to sit beside, so the box centres on the row's
|
|
153
|
+
text instead of dropping 1px to meet a label's first line. */
|
|
154
|
+
.ui-table__selection input[type="checkbox"] { margin-top: 0; vertical-align: middle; }
|
|
149
155
|
/* The target, not the ink. The box above is drawn at 19x19 because that is what
|
|
150
156
|
sits beside a 15px label without shouting; WCAG 2.5.8 wants 24 CSS px of
|
|
151
157
|
pointer target, which is a different question. This overlay answers it and
|
|
@@ -155,7 +161,8 @@
|
|
|
155
161
|
inside the same <label> (which toggles the same input, so an overlap there
|
|
156
162
|
would cost nothing) and 14px between stacked rows in the specimen. Measured
|
|
157
163
|
in stories/guidelines/accessibility-floor.test.js. #219 */
|
|
158
|
-
.ui-check input::before
|
|
164
|
+
.ui-check input::before,
|
|
165
|
+
.ui-table__selection input[type="checkbox"]::before {
|
|
159
166
|
content: '';
|
|
160
167
|
position: absolute;
|
|
161
168
|
left: 50%;
|
|
@@ -171,9 +178,12 @@
|
|
|
171
178
|
accent band is the second band #578 round r34 took off the focus rules, and a
|
|
172
179
|
pointer resting on a focused control would put it back. One indicator, which is
|
|
173
180
|
what the kit's rows already chose. why: docs/foundations.md#the-focus-ring */
|
|
174
|
-
.ui-check input:enabled:hover:not(:focus-visible)
|
|
175
|
-
.ui-
|
|
176
|
-
.ui-check input
|
|
181
|
+
.ui-check input:enabled:hover:not(:focus-visible),
|
|
182
|
+
.ui-table__selection input[type="checkbox"]:enabled:hover:not(:focus-visible) { border-color: var(--accent); }
|
|
183
|
+
.ui-check input:checked,
|
|
184
|
+
.ui-table__selection input[type="checkbox"]:checked { background: var(--accent); border-color: var(--accent); }
|
|
185
|
+
.ui-check input[type="checkbox"]:checked::after,
|
|
186
|
+
.ui-table__selection input[type="checkbox"]:checked::after {
|
|
177
187
|
content: "";
|
|
178
188
|
position: absolute;
|
|
179
189
|
left: 5px; top: 1.5px;
|
|
@@ -199,14 +209,59 @@
|
|
|
199
209
|
ground together. A checked box drops the accent for the reason a disabled primary
|
|
200
210
|
button does: the accent IS the "press me". Its mark inverts to --disabled-surface,
|
|
201
211
|
so the tick and the dot keep the measured --disabled-ink pair in both themes. */
|
|
202
|
-
.ui-check input:disabled
|
|
212
|
+
.ui-check input:disabled,
|
|
213
|
+
.ui-table__selection input[type="checkbox"]:disabled {
|
|
203
214
|
background: var(--disabled-surface); /* code-bg: inherit — control paint; the containing surface is the ground, for a chip and for a band alike. */
|
|
204
215
|
border-color: var(--disabled-border);
|
|
205
216
|
cursor: not-allowed;
|
|
206
217
|
}
|
|
207
|
-
.ui-check input:disabled:checked
|
|
208
|
-
.ui-
|
|
218
|
+
.ui-check input:disabled:checked,
|
|
219
|
+
.ui-table__selection input[type="checkbox"]:disabled:checked { background: var(--disabled-ink); border-color: var(--disabled-ink); }
|
|
220
|
+
.ui-check input[type="checkbox"]:disabled:checked::after,
|
|
221
|
+
.ui-table__selection input[type="checkbox"]:disabled:checked::after { border-color: var(--disabled-surface); }
|
|
209
222
|
.ui-check input[type="radio"]:disabled:checked::after { background: var(--disabled-surface); /* code-bg: inherit — the mark inside the box; the box carries the focus indicator. */ }
|
|
223
|
+
/* Partial: some of a group is on, not all. The checked box's fill with a level
|
|
224
|
+
dash where the tick would be, so the box still reads as "on" from across a table
|
|
225
|
+
and the dash alone says "not all". It wins over a tick when both are set, as the
|
|
226
|
+
browser's own box does. Drawn with a border, not a background, so forced colours
|
|
227
|
+
keeps the dash. #619 */
|
|
228
|
+
.ui-check input[type="checkbox"]:indeterminate,
|
|
229
|
+
.ui-table__selection input[type="checkbox"]:indeterminate { background: var(--accent); border-color: var(--accent); }
|
|
230
|
+
.ui-check input[type="checkbox"]:indeterminate::after,
|
|
231
|
+
.ui-table__selection input[type="checkbox"]:indeterminate::after {
|
|
232
|
+
content: "";
|
|
233
|
+
position: absolute;
|
|
234
|
+
inset: 0;
|
|
235
|
+
margin: auto;
|
|
236
|
+
width: 11px; height: 0; /* odd, so the 17px padding box splits into equal gaps */
|
|
237
|
+
border: solid var(--accent-contrast);
|
|
238
|
+
border-width: 2px 0 0;
|
|
239
|
+
transform: none;
|
|
240
|
+
}
|
|
241
|
+
/* White ink falls below 3:1 on the lighter dark-theme accents, so the tick, the dash and
|
|
242
|
+
the radio's dot take the ink a selected Segmented pill takes there. */
|
|
243
|
+
:where(:root[data-theme="dark"]) .ui-check input[type="checkbox"]:enabled:is(:checked, :indeterminate)::after,
|
|
244
|
+
:where(:root[data-theme="dark"]) .ui-table__selection input[type="checkbox"]:enabled:is(:checked, :indeterminate)::after { border-color: var(--signal-contrast); }
|
|
245
|
+
:where(:root[data-theme="dark"]) .ui-check input[type="radio"]:enabled:checked::after { background: var(--signal-contrast); }
|
|
246
|
+
.ui-check input[type="checkbox"]:disabled:indeterminate,
|
|
247
|
+
.ui-table__selection input[type="checkbox"]:disabled:indeterminate { background: var(--disabled-ink); border-color: var(--disabled-ink); }
|
|
248
|
+
.ui-check input[type="checkbox"]:disabled:indeterminate::after,
|
|
249
|
+
.ui-table__selection input[type="checkbox"]:disabled:indeterminate::after { border-color: var(--disabled-surface); }
|
|
250
|
+
/* Forced colours replace every fill, so a disabled box that painted only a fill and
|
|
251
|
+
a --disabled-* edge drew the same edge and mark as an enabled one, and a radio's dot
|
|
252
|
+
vanished. GrayText is the system colour for an unavailable control. The tick and the
|
|
253
|
+
dash are borders, so they keep it; the dot is a fill, so it names a system colour. */
|
|
254
|
+
@media (forced-colors: active) {
|
|
255
|
+
.ui-check input:disabled,
|
|
256
|
+
.ui-check input:disabled:is(:checked, :indeterminate),
|
|
257
|
+
.ui-check input[type="checkbox"]:disabled:is(:checked, :indeterminate),
|
|
258
|
+
.ui-table__selection input[type="checkbox"]:disabled,
|
|
259
|
+
.ui-table__selection input[type="checkbox"]:disabled:is(:checked, :indeterminate) { border-color: GrayText; }
|
|
260
|
+
.ui-check input[type="checkbox"]:disabled:is(:checked, :indeterminate)::after,
|
|
261
|
+
.ui-table__selection input[type="checkbox"]:disabled:is(:checked, :indeterminate)::after { border-color: GrayText; }
|
|
262
|
+
.ui-check input[type="radio"]:enabled:checked::after { background: CanvasText; }
|
|
263
|
+
.ui-check input[type="radio"]:disabled:checked::after { background: GrayText; }
|
|
264
|
+
}
|
|
210
265
|
/* The label is the click target, so the cursor belongs on it, and the words it
|
|
211
266
|
holds go quiet with the box beside them. */
|
|
212
267
|
.ui-check:has(input:disabled) { /* muted-ink: state — unavailable control */ color: var(--disabled-ink); cursor: not-allowed; }
|
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; }
|
package/src/styles/table.css
CHANGED
|
@@ -467,6 +467,9 @@ a.ui-identity:hover { text-decoration: underline; }
|
|
|
467
467
|
.ui-table--pinned .ui-identity__name { min-width: 0; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
/* Automatic layout hands every column a share of the free width, and a box is not worth one: the selection column takes the width of its box and the gap after it. The pinned table below fixes it instead. */
|
|
471
|
+
.ui-table .ui-table__selection { width: 1%; }
|
|
472
|
+
|
|
470
473
|
/* One width reserves the selection track and offsets the adjacent identity. */
|
|
471
474
|
.ui-table--pinned { --ui-table-selection-width: var(--space-12); --ui-table-identity-max: min(var(--panel-sm), 50vw); }
|
|
472
475
|
.ui-table--pinned .ui-table__selection { box-sizing: border-box; width: var(--ui-table-selection-width); min-width: var(--ui-table-selection-width); max-width: var(--ui-table-selection-width); padding-inline: 0; text-align: center; }
|
package/src/styles/tap-zone.css
CHANGED
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
with a 1.5px border, so without it the clamp takes this layer DOWN to 22
|
|
97
97
|
and under the kit's own minimum. */
|
|
98
98
|
.ui-check input::before,
|
|
99
|
+
.ui-table__selection input[type="checkbox"]::before,
|
|
99
100
|
.ui-toast__close::before {
|
|
100
101
|
width: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x))));
|
|
101
102
|
height: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y))));
|
|
@@ -168,6 +169,12 @@
|
|
|
168
169
|
}
|
|
169
170
|
.ui-table__act { --tap-clear-x: var(--tap-gap); --tap-clear-y: 0px; }
|
|
170
171
|
|
|
172
|
+
/* The selection box is 19px in a 58px row and a 34px header: the layer has room to reach the floor both ways. */
|
|
173
|
+
.ui-table__selection { --tap-clear-x: var(--tap-min); --tap-clear-y: var(--tap-min); }
|
|
174
|
+
/* A dense or compact table's rows are 33px to 40px apart and share an edge, so a layer that grows down takes the pixels nearest the next row's box. Those rows keep the 24px floor. */
|
|
175
|
+
.ui-table--dense .ui-table__selection,
|
|
176
|
+
.ui-table--compact .ui-table__selection { --tap-clear-y: 0px; }
|
|
177
|
+
|
|
171
178
|
/* Down only, like the nine rows below: a toast is a full row at this width,
|
|
172
179
|
and opening it across took 24px out of the message for no reach at all —
|
|
173
180
|
its action already clears the floor on the vertical half. The box does not
|