@gtivr4/a1-design-system-react 0.14.0 → 0.18.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 +3 -2
- package/src/color-scheme.css +2 -0
- package/src/components/accordion/Accordion.d.ts +8 -0
- package/src/components/accordion/Accordion.jsx +9 -1
- package/src/components/accordion/accordion.css +46 -6
- package/src/components/autocomplete/Autocomplete.d.ts +53 -0
- package/src/components/autocomplete/Autocomplete.jsx +380 -0
- package/src/components/autocomplete/autocomplete.css +346 -0
- package/src/components/banner/Banner.d.ts +9 -2
- package/src/components/banner/Banner.jsx +32 -6
- package/src/components/banner/banner.css +81 -0
- package/src/components/bottom-sheet/BottomSheet.d.ts +22 -0
- package/src/components/bottom-sheet/BottomSheet.jsx +154 -0
- package/src/components/bottom-sheet/bottom-sheet.css +113 -0
- package/src/components/button/button.css +7 -3
- package/src/components/code/Code.jsx +6 -1
- package/src/components/data-table/DataTable.jsx +11 -1
- package/src/components/data-table/data-table.css +19 -0
- package/src/components/figure/Figure.d.ts +37 -4
- package/src/components/figure/Figure.jsx +78 -9
- package/src/components/figure/figure.css +105 -8
- package/src/components/grid/Grid.d.ts +1 -1
- package/src/components/grid/Grid.jsx +2 -0
- package/src/components/grid/grid.css +5 -0
- package/src/components/icon-button/IconButton.d.ts +2 -2
- package/src/components/icon-button/IconButton.jsx +3 -2
- package/src/components/icon-button/icon-button.css +11 -1
- package/src/components/menu/Menu.jsx +12 -0
- package/src/components/menu/menu.css +17 -6
- package/src/components/page-layout/page-layout.css +10 -4
- package/src/components/page-nav/PageNav.jsx +29 -8
- package/src/components/page-nav/page-nav.css +13 -0
- package/src/components/paragraph/Paragraph.d.ts +2 -0
- package/src/components/paragraph/Paragraph.jsx +4 -0
- package/src/components/paragraph/paragraph.css +6 -6
- package/src/components/section/Section.d.ts +6 -0
- package/src/components/section/Section.jsx +19 -0
- package/src/components/section/section.css +33 -10
- package/src/components/segmented-control/SegmentedControl.d.ts +8 -0
- package/src/components/segmented-control/SegmentedControl.jsx +16 -3
- package/src/components/segmented-control/segmented.css +31 -1
- package/src/components/slider/Slider.d.ts +71 -0
- package/src/components/slider/Slider.jsx +243 -0
- package/src/components/slider/slider.css +238 -0
- package/src/components/split-button/SplitButton.d.ts +39 -0
- package/src/components/split-button/SplitButton.jsx +94 -0
- package/src/components/split-button/split-button.css +40 -0
- package/src/components/tabs/tabs.css +3 -0
- package/src/components/toolbar/Toolbar.d.ts +131 -0
- package/src/components/toolbar/Toolbar.jsx +335 -0
- package/src/components/toolbar/toolbar.css +229 -0
- package/src/components/top-header/top-header.css +2 -0
- package/src/components/tree-menu/TreeMenu.jsx +11 -7
- package/src/index.d.ts +71 -0
- package/src/index.js +15 -1
- package/src/themes.css +293 -0
- package/src/tokens.css +26 -3
|
@@ -37,12 +37,22 @@
|
|
|
37
37
|
--a1-icon-opsz: var(--a1-icon-button-icon-opsz, var(--component-icon-button-icon-optical-size));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/* ── Size: small ─────────────────────────────────────────────────────────── */
|
|
41
|
+
/* 24×24px total — the WCAG 2.2 target-size (AA) minimum; for dense toolbars. */
|
|
42
|
+
|
|
43
|
+
.a1-icon-button--small {
|
|
44
|
+
--a1-icon-button-size: var(--base-spacing-24);
|
|
45
|
+
--a1-icon-button-icon-size: var(--base-spacing-16);
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
/* ── Size: large ─────────────────────────────────────────────────────────── */
|
|
41
49
|
/* Touch target matches Button lg (3.5rem); icon matches Button lg's icon. */
|
|
42
50
|
|
|
43
51
|
.a1-icon-button--large {
|
|
44
52
|
--a1-icon-button-size: var(--component-button-large-height);
|
|
45
|
-
|
|
53
|
+
/* Icon is two steps up the icon scale (20 → 24 → 32px) so it reads at the
|
|
54
|
+
larger touch target. */
|
|
55
|
+
--a1-icon-button-icon-size: var(--base-spacing-32);
|
|
46
56
|
--a1-icon-button-icon-opsz: var(--component-button-icon-optical-size);
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -81,6 +81,18 @@ export function Menu({
|
|
|
81
81
|
el.style.setProperty("--a1-menu-top", `${Math.round(top)}px`);
|
|
82
82
|
el.style.setProperty("--a1-menu-left", `${Math.round(left)}px`);
|
|
83
83
|
el.style.setProperty("--a1-menu-max-height", `${Math.floor(maxHeight)}px`);
|
|
84
|
+
|
|
85
|
+
// `position: fixed` resolves against the nearest transformed/filtered
|
|
86
|
+
// ancestor's box, not the viewport — e.g. an `overlay` Toolbar positioned
|
|
87
|
+
// with a CSS transform. Measure where the menu actually landed and correct
|
|
88
|
+
// by the containing-block offset so it stays anchored to the viewport.
|
|
89
|
+
const placed = el.getBoundingClientRect();
|
|
90
|
+
const dx = left - placed.left;
|
|
91
|
+
const dy = top - placed.top;
|
|
92
|
+
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
|
|
93
|
+
el.style.setProperty("--a1-menu-left", `${Math.round(left + dx)}px`);
|
|
94
|
+
el.style.setProperty("--a1-menu-top", `${Math.round(top + dy)}px`);
|
|
95
|
+
}
|
|
84
96
|
}, [anchorRef]);
|
|
85
97
|
|
|
86
98
|
const openDialog = useCallback(() => {
|
|
@@ -104,16 +104,27 @@
|
|
|
104
104
|
background: transparent;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/* Active / current-page indicator
|
|
107
|
+
/* Active / current-page indicator — matches the TreeMenu selected pattern:
|
|
108
|
+
a solid action-background fill for a clear, unambiguous highlight. */
|
|
108
109
|
.a1-menu-item--active {
|
|
109
|
-
color: var(--semantic-color-
|
|
110
|
-
background: var(--semantic-color-action-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
color: var(--semantic-color-action-foreground);
|
|
111
|
+
background: var(--semantic-color-action-background);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.a1-menu-item--active:hover {
|
|
115
|
+
background: var(--semantic-color-action-background-hover);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.a1-menu-item--active:active {
|
|
119
|
+
background: var(--semantic-color-action-background-pressed);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.a1-menu-item--active:focus-visible {
|
|
123
|
+
outline-color: var(--semantic-color-action-foreground);
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
.a1-menu-item--active .a1-menu-item__icon {
|
|
116
|
-
color: var(--semantic-color-action-
|
|
127
|
+
color: var(--semantic-color-action-foreground);
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
/* Destructive variant */
|
|
@@ -117,10 +117,16 @@
|
|
|
117
117
|
overflow-y: hidden;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
/* Only at lg+ is the sidebar persistent and in flow; pin the SideNav to fill the
|
|
121
|
+
sidebar height so its footer stays anchored. At md and below the SideNav must
|
|
122
|
+
keep its fixed-overlay behaviour (see side-nav.css) so the sidebar slot
|
|
123
|
+
collapses and the main column expands to fill the freed space. */
|
|
124
|
+
@media (--bp-lg-up) {
|
|
125
|
+
.a1-page-layout--viewport-height .a1-page-layout__sidebar .a1-side-nav {
|
|
126
|
+
position: relative;
|
|
127
|
+
top: auto;
|
|
128
|
+
height: 100%;
|
|
129
|
+
}
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
.a1-page-layout--viewport-height .a1-page-layout__content {
|
|
@@ -2,6 +2,22 @@ import { useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { Card } from "../card/Card.jsx";
|
|
3
3
|
import "./page-nav.css";
|
|
4
4
|
|
|
5
|
+
// Find the nearest scrollable ancestor of a node, or null when the page scrolls
|
|
6
|
+
// on the document/window itself. Needed because the host may scroll a nested
|
|
7
|
+
// container (e.g. a viewport-height PageLayout) rather than the window — in which
|
|
8
|
+
// case window scroll never fires and the progress/active state would freeze.
|
|
9
|
+
function getScrollParent(node) {
|
|
10
|
+
let el = node?.parentElement;
|
|
11
|
+
while (el && el !== document.body && el !== document.documentElement) {
|
|
12
|
+
const overflowY = getComputedStyle(el).overflowY;
|
|
13
|
+
if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
el = el.parentElement;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
export function PageNav({
|
|
6
22
|
sections = [],
|
|
7
23
|
label = "On this page",
|
|
@@ -12,22 +28,27 @@ export function PageNav({
|
|
|
12
28
|
const [progress, setProgress] = useState(0);
|
|
13
29
|
const intersectingIds = useRef(new Set());
|
|
14
30
|
|
|
15
|
-
// Reading progress: track
|
|
31
|
+
// Reading progress: track the scroll position of the actual scroll container
|
|
32
|
+
// (the window, or a nested scrollable ancestor like a viewport-height layout).
|
|
16
33
|
useEffect(() => {
|
|
34
|
+
const scroller = getScrollParent(sections.length ? document.getElementById(sections[0].id) : null);
|
|
35
|
+
const target = scroller ?? window;
|
|
17
36
|
function update() {
|
|
18
|
-
const el = document.documentElement;
|
|
37
|
+
const el = scroller ?? document.documentElement;
|
|
19
38
|
const total = el.scrollHeight - el.clientHeight;
|
|
20
39
|
setProgress(total > 0 ? (el.scrollTop / total) * 100 : 0);
|
|
21
40
|
}
|
|
22
|
-
|
|
41
|
+
target.addEventListener("scroll", update, { passive: true });
|
|
23
42
|
update();
|
|
24
|
-
return () =>
|
|
25
|
-
}, []);
|
|
43
|
+
return () => target.removeEventListener("scroll", update);
|
|
44
|
+
}, [sections]);
|
|
26
45
|
|
|
27
|
-
// Active section: observe each section element entering/leaving the
|
|
46
|
+
// Active section: observe each section element entering/leaving the scroll
|
|
47
|
+
// container (root = the nested scroller, or the viewport when null).
|
|
28
48
|
useEffect(() => {
|
|
29
49
|
if (!sections.length) return;
|
|
30
50
|
|
|
51
|
+
const scroller = getScrollParent(document.getElementById(sections[0].id));
|
|
31
52
|
const observer = new IntersectionObserver(
|
|
32
53
|
(entries) => {
|
|
33
54
|
entries.forEach((entry) => {
|
|
@@ -43,9 +64,9 @@ export function PageNav({
|
|
|
43
64
|
if (first) setActiveId(first.id);
|
|
44
65
|
},
|
|
45
66
|
// -8% top offset keeps the active section stable once it clears the
|
|
46
|
-
// header; -88% bottom offset means only the top 12% of the
|
|
67
|
+
// header; -88% bottom offset means only the top 12% of the scroll area
|
|
47
68
|
// is treated as "current".
|
|
48
|
-
{ rootMargin: "-8% 0px -88% 0px", threshold: 0 }
|
|
69
|
+
{ root: scroller ?? null, rootMargin: "-8% 0px -88% 0px", threshold: 0 }
|
|
49
70
|
);
|
|
50
71
|
|
|
51
72
|
sections.forEach(({ id }) => {
|
|
@@ -163,3 +163,16 @@
|
|
|
163
163
|
border-color: var(--semantic-color-action-background);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
/* ── Desktop: stick within its column as the reader scrolls ────────────────── */
|
|
168
|
+
/* `--a1-page-nav-top` lets a consumer offset for a sticky header (default 16px);
|
|
169
|
+
a long nav scrolls internally rather than running past the viewport. */
|
|
170
|
+
@media (min-width: 769px) {
|
|
171
|
+
.a1-page-nav.a1-card {
|
|
172
|
+
position: sticky;
|
|
173
|
+
top: var(--a1-page-nav-top, var(--base-spacing-16));
|
|
174
|
+
align-self: start;
|
|
175
|
+
max-block-size: calc(100vh - var(--a1-page-nav-top, var(--base-spacing-16)) - var(--base-spacing-16));
|
|
176
|
+
overflow: hidden auto;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -17,6 +17,8 @@ export interface ParagraphProps extends React.HTMLAttributes<HTMLParagraphElemen
|
|
|
17
17
|
textWrap?: "balance";
|
|
18
18
|
/** Horizontal text alignment. "start"/"end" are logical aliases for LTR/RTL-safe alignment. */
|
|
19
19
|
align?: "left" | "center" | "right" | "start" | "end";
|
|
20
|
+
/** Font weight. Omit to inherit the body default. */
|
|
21
|
+
weight?: "regular" | "medium" | "semibold" | "bold";
|
|
20
22
|
children?: React.ReactNode;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -5,6 +5,7 @@ const colors = ["default", "muted"];
|
|
|
5
5
|
const breakpoints = ["xs", "sm", "md", "lg", "xl"];
|
|
6
6
|
const textWraps = ["balance"];
|
|
7
7
|
const aligns = ["left", "center", "right", "start", "end"];
|
|
8
|
+
const weights = ["regular", "medium", "semibold", "bold"];
|
|
8
9
|
|
|
9
10
|
function isResponsiveSize(size) {
|
|
10
11
|
return size && typeof size === "object" && !Array.isArray(size);
|
|
@@ -31,6 +32,7 @@ export function Paragraph({
|
|
|
31
32
|
color = "default",
|
|
32
33
|
textWrap,
|
|
33
34
|
align,
|
|
35
|
+
weight,
|
|
34
36
|
className = "",
|
|
35
37
|
style,
|
|
36
38
|
...props
|
|
@@ -39,6 +41,7 @@ export function Paragraph({
|
|
|
39
41
|
const resolvedColor = colors.includes(color) ? color : "default";
|
|
40
42
|
const resolvedTextWrap = textWraps.includes(textWrap) ? textWrap : null;
|
|
41
43
|
const resolvedAlign = aligns.includes(align) ? align : null;
|
|
44
|
+
const resolvedWeight = weights.includes(weight) ? weight : null;
|
|
42
45
|
const responsiveStyle = getResponsiveSizeStyle(size);
|
|
43
46
|
const resolvedStyle = Object.keys(responsiveStyle).length
|
|
44
47
|
? { ...responsiveStyle, ...style }
|
|
@@ -50,6 +53,7 @@ export function Paragraph({
|
|
|
50
53
|
resolvedColor !== "default" && `a1-paragraph--${resolvedColor}`,
|
|
51
54
|
resolvedTextWrap && `a1-paragraph--wrap-${resolvedTextWrap}`,
|
|
52
55
|
resolvedAlign && `a1-paragraph--align-${resolvedAlign}`,
|
|
56
|
+
resolvedWeight && `a1-paragraph--weight-${resolvedWeight}`,
|
|
53
57
|
className
|
|
54
58
|
]
|
|
55
59
|
.filter(Boolean)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
margin: 0;
|
|
3
3
|
font-family: var(--component-paragraph-font-family);
|
|
4
4
|
font-size: var(--a1-paragraph-responsive-size, var(--a1-paragraph-size));
|
|
5
|
-
font-weight: var(--component-paragraph-font-weight);
|
|
5
|
+
font-weight: var(--a1-paragraph-weight, var(--component-paragraph-font-weight));
|
|
6
6
|
line-height: var(--component-paragraph-font-line-height);
|
|
7
7
|
color: var(--a1-paragraph-color, var(--semantic-color-text-default));
|
|
8
8
|
}
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
|
|
40
40
|
.a1-paragraph--muted { --a1-paragraph-color: var(--semantic-color-text-muted); }
|
|
41
41
|
|
|
42
|
+
.a1-paragraph--weight-regular { --a1-paragraph-weight: var(--base-font-weight-regular); }
|
|
43
|
+
.a1-paragraph--weight-medium { --a1-paragraph-weight: var(--base-font-weight-medium); }
|
|
44
|
+
.a1-paragraph--weight-semibold { --a1-paragraph-weight: var(--base-font-weight-semibold); }
|
|
45
|
+
.a1-paragraph--weight-bold { --a1-paragraph-weight: var(--base-font-weight-bold); }
|
|
46
|
+
|
|
42
47
|
/* Text wrap */
|
|
43
48
|
.a1-paragraph--wrap-balance { text-wrap: balance; }
|
|
44
49
|
|
|
@@ -48,8 +53,3 @@
|
|
|
48
53
|
.a1-paragraph--align-right { text-align: end; }
|
|
49
54
|
.a1-paragraph--align-start { text-align: start; }
|
|
50
55
|
.a1-paragraph--align-end { text-align: end; }
|
|
51
|
-
|
|
52
|
-
.a1-paragraph + .a1-paragraph,
|
|
53
|
-
.a1-paragraph + .a1-heading {
|
|
54
|
-
margin-top: 1.5em;
|
|
55
|
-
}
|
|
@@ -33,6 +33,12 @@ export interface SectionProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
33
33
|
borderStyle?: "solid" | "dashed" | "dotted";
|
|
34
34
|
/** Border color tone. Uses the same variants as Divider. Default: "subtle" */
|
|
35
35
|
borderVariant?: "subtle" | "strong" | "accent";
|
|
36
|
+
/**
|
|
37
|
+
* Which sides the border is drawn on (requires `borderSize`). `"all"` (default)
|
|
38
|
+
* draws all four sides; pass an array to draw only those sides, e.g.
|
|
39
|
+
* `["top", "bottom"]`. An empty array draws no border.
|
|
40
|
+
*/
|
|
41
|
+
borderSides?: "all" | ("top" | "right" | "bottom" | "left")[];
|
|
36
42
|
/** Border radius scale. */
|
|
37
43
|
radius?: "none" | "sm" | "md" | "lg" | "xl";
|
|
38
44
|
children?: React.ReactNode;
|
|
@@ -23,8 +23,19 @@ const VALID_ALIGNMENTS = ["left", "center", "right"];
|
|
|
23
23
|
const VALID_BORDER_SIZES = ["xs", "sm", "md", "lg"];
|
|
24
24
|
const VALID_BORDER_STYLES = ["solid", "dashed", "dotted"];
|
|
25
25
|
const VALID_BORDER_VARIANTS = ["subtle", "strong", "accent"];
|
|
26
|
+
const VALID_BORDER_SIDES = ["top", "right", "bottom", "left"];
|
|
26
27
|
const VALID_RADII = ["none", "sm", "md", "lg", "xl"];
|
|
27
28
|
|
|
29
|
+
// Resolve which sides get the border. `null` means all sides (the default);
|
|
30
|
+
// an array means only those sides (an empty array means no border).
|
|
31
|
+
function resolveBorderSides(borderSides) {
|
|
32
|
+
if (borderSides == null || borderSides === "all") return null;
|
|
33
|
+
const arr = Array.isArray(borderSides) ? borderSides : [borderSides];
|
|
34
|
+
const sides = VALID_BORDER_SIDES.filter((side) => arr.includes(side));
|
|
35
|
+
if (sides.length === VALID_BORDER_SIDES.length) return null; // all four = all
|
|
36
|
+
return sides;
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export function Section({
|
|
29
40
|
as: Component = "section",
|
|
30
41
|
padding = "md",
|
|
@@ -39,6 +50,7 @@ export function Section({
|
|
|
39
50
|
borderSize,
|
|
40
51
|
borderStyle = "solid",
|
|
41
52
|
borderVariant = "subtle",
|
|
53
|
+
borderSides,
|
|
42
54
|
radius,
|
|
43
55
|
className = "",
|
|
44
56
|
children,
|
|
@@ -98,6 +110,13 @@ export function Section({
|
|
|
98
110
|
|
|
99
111
|
if (borderSize && VALID_BORDER_SIZES.includes(borderSize)) {
|
|
100
112
|
classes.push(`a1-section--border-${borderSize}`);
|
|
113
|
+
|
|
114
|
+
// Per-side borders. Omitted / "all" draws all four sides (default).
|
|
115
|
+
const sides = resolveBorderSides(borderSides);
|
|
116
|
+
if (sides) {
|
|
117
|
+
classes.push("a1-section--border-sided");
|
|
118
|
+
sides.forEach((side) => classes.push(`a1-section--border-side-${side}`));
|
|
119
|
+
}
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
if (borderStyle && VALID_BORDER_STYLES.includes(borderStyle)) {
|
|
@@ -13,7 +13,15 @@
|
|
|
13
13
|
--a1-section-border-size: 0;
|
|
14
14
|
--a1-section-border-style: solid;
|
|
15
15
|
--a1-section-border-color: transparent;
|
|
16
|
-
|
|
16
|
+
/* Per-side widths default to the full border size (all sides). The sided
|
|
17
|
+
modifier zeroes them so individual sides can be opted back in. */
|
|
18
|
+
--a1-section-border-top: var(--a1-section-border-size);
|
|
19
|
+
--a1-section-border-right: var(--a1-section-border-size);
|
|
20
|
+
--a1-section-border-bottom: var(--a1-section-border-size);
|
|
21
|
+
--a1-section-border-left: var(--a1-section-border-size);
|
|
22
|
+
border-style: var(--a1-section-border-style);
|
|
23
|
+
border-color: var(--a1-section-border-color);
|
|
24
|
+
border-width: var(--a1-section-border-top) var(--a1-section-border-right) var(--a1-section-border-bottom) var(--a1-section-border-left);
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
.a1-section.a1-inverse {
|
|
@@ -42,6 +50,18 @@
|
|
|
42
50
|
.a1-section--border-strong { --a1-section-border-color: var(--semantic-color-border-strong); }
|
|
43
51
|
.a1-section--border-accent { --a1-section-border-color: var(--semantic-color-text-accent); }
|
|
44
52
|
|
|
53
|
+
/* Per-side borders: start from no sides, then opt each chosen side back in. */
|
|
54
|
+
.a1-section--border-sided {
|
|
55
|
+
--a1-section-border-top: 0;
|
|
56
|
+
--a1-section-border-right: 0;
|
|
57
|
+
--a1-section-border-bottom: 0;
|
|
58
|
+
--a1-section-border-left: 0;
|
|
59
|
+
}
|
|
60
|
+
.a1-section--border-side-top { --a1-section-border-top: var(--a1-section-border-size); }
|
|
61
|
+
.a1-section--border-side-right { --a1-section-border-right: var(--a1-section-border-size); }
|
|
62
|
+
.a1-section--border-side-bottom { --a1-section-border-bottom: var(--a1-section-border-size); }
|
|
63
|
+
.a1-section--border-side-left { --a1-section-border-left: var(--a1-section-border-size); }
|
|
64
|
+
|
|
45
65
|
/* ── Radius ────────────────────────────────────────────────────────────────── */
|
|
46
66
|
|
|
47
67
|
.a1-section--radius-none { border-radius: 0; }
|
|
@@ -92,21 +112,24 @@
|
|
|
92
112
|
|
|
93
113
|
/* ── Content gap ──────────────────────────────────────────────────────────── */
|
|
94
114
|
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
justify-items: var(--a1-section-justify-items);
|
|
102
|
-
}
|
|
103
|
-
|
|
115
|
+
/* A gap is the grid gap only when the section is a grid — i.e. when it is
|
|
116
|
+
aligned (or height-hero). Without alignment the section flows as block
|
|
117
|
+
(block children fill the width; inline content keeps its natural size) and
|
|
118
|
+
the gap becomes vertical rhythm via margins, since `gap` has no effect on
|
|
119
|
+
display:block. This keeps natural-width content (a Button, a Badge) from
|
|
120
|
+
being stretched by the grid when no alignment is requested. */
|
|
104
121
|
.a1-section--gap-xs { gap: var(--semantic-spacing-gap-xs); }
|
|
105
122
|
.a1-section--gap-sm { gap: var(--semantic-spacing-gap-sm); }
|
|
106
123
|
.a1-section--gap-md { gap: var(--semantic-spacing-gap-md); }
|
|
107
124
|
.a1-section--gap-lg { gap: var(--semantic-spacing-gap-lg); }
|
|
108
125
|
.a1-section--gap-xl { gap: var(--semantic-spacing-gap-xl); }
|
|
109
126
|
|
|
127
|
+
.a1-section--gap-xs:not([class*="-align-"]):not(.a1-section--height-hero) > * + * { margin-block-start: var(--semantic-spacing-gap-xs); }
|
|
128
|
+
.a1-section--gap-sm:not([class*="-align-"]):not(.a1-section--height-hero) > * + * { margin-block-start: var(--semantic-spacing-gap-sm); }
|
|
129
|
+
.a1-section--gap-md:not([class*="-align-"]):not(.a1-section--height-hero) > * + * { margin-block-start: var(--semantic-spacing-gap-md); }
|
|
130
|
+
.a1-section--gap-lg:not([class*="-align-"]):not(.a1-section--height-hero) > * + * { margin-block-start: var(--semantic-spacing-gap-lg); }
|
|
131
|
+
.a1-section--gap-xl:not([class*="-align-"]):not(.a1-section--height-hero) > * + * { margin-block-start: var(--semantic-spacing-gap-xl); }
|
|
132
|
+
|
|
110
133
|
/* ── Height ────────────────────────────────────────────────────────────────── */
|
|
111
134
|
|
|
112
135
|
.a1-section--height-screen {
|
|
@@ -18,6 +18,14 @@ export interface SegmentedControlProps extends React.HTMLAttributes<HTMLDivEleme
|
|
|
18
18
|
fullWidth?: boolean;
|
|
19
19
|
/** Height scale. Default: "md" */
|
|
20
20
|
size?: "sm" | "md" | "lg";
|
|
21
|
+
/**
|
|
22
|
+
* Label display. `"all"` (default) shows every option's label. `"selected"`
|
|
23
|
+
* shows the label only on the selected option; the rest render icon-only
|
|
24
|
+
* (using each option's `ariaLabel`/`label` for its accessible name). Options
|
|
25
|
+
* without an icon always show their label so they never render blank.
|
|
26
|
+
* "none" hides every label (fully icon-only).
|
|
27
|
+
*/
|
|
28
|
+
labelMode?: "all" | "selected" | "none";
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
export declare function SegmentedControl(props: SegmentedControlProps): React.ReactElement;
|
|
@@ -11,6 +11,7 @@ export function SegmentedControl({
|
|
|
11
11
|
onChange,
|
|
12
12
|
fullWidth = false,
|
|
13
13
|
size,
|
|
14
|
+
labelMode = "all",
|
|
14
15
|
...props
|
|
15
16
|
}) {
|
|
16
17
|
const items = options.map(normalize);
|
|
@@ -52,8 +53,20 @@ export function SegmentedControl({
|
|
|
52
53
|
onKeyDown={handleKeyDown}
|
|
53
54
|
>
|
|
54
55
|
{items.map((opt) => {
|
|
55
|
-
const iconOnly = Boolean(opt.icon) && !opt.label;
|
|
56
56
|
const isSelected = value === opt.value;
|
|
57
|
+
// labelMode controls which segments show their text label:
|
|
58
|
+
// "all" (default) — every segment.
|
|
59
|
+
// "selected" — only the selected segment.
|
|
60
|
+
// "none" — none of them (fully icon-only).
|
|
61
|
+
// An option with no icon always shows its label so it never goes blank.
|
|
62
|
+
const wantsLabel =
|
|
63
|
+
Boolean(opt.label) &&
|
|
64
|
+
(labelMode === "none"
|
|
65
|
+
? !opt.icon
|
|
66
|
+
: labelMode === "selected"
|
|
67
|
+
? (isSelected || !opt.icon)
|
|
68
|
+
: true);
|
|
69
|
+
const iconOnly = Boolean(opt.icon) && !wantsLabel;
|
|
57
70
|
|
|
58
71
|
return (
|
|
59
72
|
<button
|
|
@@ -61,7 +74,7 @@ export function SegmentedControl({
|
|
|
61
74
|
role="radio"
|
|
62
75
|
type="button"
|
|
63
76
|
aria-checked={isSelected}
|
|
64
|
-
aria-label={iconOnly ? (opt.ariaLabel ?? opt.value) : undefined}
|
|
77
|
+
aria-label={iconOnly ? (opt.ariaLabel ?? opt.label ?? opt.value) : undefined}
|
|
65
78
|
tabIndex={isSelected ? 0 : -1}
|
|
66
79
|
className={[
|
|
67
80
|
"a1-segment",
|
|
@@ -72,7 +85,7 @@ export function SegmentedControl({
|
|
|
72
85
|
onClick={() => onChange?.(opt.value)}
|
|
73
86
|
>
|
|
74
87
|
{opt.icon && <Icon name={opt.icon} className="a1-segment__icon" />}
|
|
75
|
-
{opt.label}
|
|
88
|
+
{wantsLabel ? opt.label : null}
|
|
76
89
|
</button>
|
|
77
90
|
);
|
|
78
91
|
})}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
.a1-segmented--full-width {
|
|
15
15
|
display: flex;
|
|
16
|
+
/* Fill the container so the equal-width segments actually stretch — `display:
|
|
17
|
+
flex` alone leaves it content-width inside a centering flex/grid parent. */
|
|
18
|
+
inline-size: 100%;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/* ─── Segment button ──────────────────────────────────────────────────────── */
|
|
@@ -58,10 +61,27 @@
|
|
|
58
61
|
font-size: var(--semantic-font-size-body-sm);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/* Small: tighter padding and a font size one step down from the default. */
|
|
65
|
+
.a1-segmented--sm .a1-segment {
|
|
66
|
+
padding: var(--component-segmented-segment-padding-block-sm)
|
|
67
|
+
var(--component-segmented-segment-padding-inline-sm);
|
|
68
|
+
font-size: var(--semantic-font-size-body-xs);
|
|
69
|
+
/* Tighter icon↔label gap at sm. */
|
|
70
|
+
gap: var(--base-spacing-4);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Large: roomier padding and a font size one step up. */
|
|
74
|
+
.a1-segmented--lg .a1-segment {
|
|
75
|
+
padding: var(--component-segmented-segment-padding-block-lg)
|
|
76
|
+
var(--component-segmented-segment-padding-inline-lg);
|
|
77
|
+
font-size: var(--semantic-font-size-body-md);
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
/* ─── Icon ────────────────────────────────────────────────────────────────── */
|
|
62
81
|
|
|
63
82
|
.a1-segment__icon {
|
|
64
|
-
|
|
83
|
+
/* One step larger than the label so the glyph reads clearly at the md/lg sizes. */
|
|
84
|
+
font-size: 1.25em;
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
/* Icon-only: match inline padding to block padding so the segment is square */
|
|
@@ -69,6 +89,16 @@
|
|
|
69
89
|
padding-inline: var(--component-segmented-segment-padding-block);
|
|
70
90
|
}
|
|
71
91
|
|
|
92
|
+
/* Small: a larger icon and tighter horizontal padding per segment (so an
|
|
93
|
+
icon-only sm strip stays compact). */
|
|
94
|
+
.a1-segmented--sm .a1-segment__icon {
|
|
95
|
+
font-size: 1.5em;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.a1-segmented--sm .a1-segment--icon-only {
|
|
99
|
+
padding-inline: var(--component-segmented-segment-padding-block-sm);
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
/* ─── Dark mode ───────────────────────────────────────────────────────────── */
|
|
73
103
|
|
|
74
104
|
.a1-theme-dark .a1-segment:not([aria-checked="true"]),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A detent (snap stop): a bare value, or a value with an optional display `label`
|
|
5
|
+
* and/or `icon` (a Material Symbols name shown in the label row instead of text).
|
|
6
|
+
* Provide a `label` alongside an `icon` to give screen readers a text alternative.
|
|
7
|
+
*/
|
|
8
|
+
export type SliderDetent = number | { value: number; label?: React.ReactNode; icon?: string };
|
|
9
|
+
|
|
10
|
+
export interface SliderProps
|
|
11
|
+
extends Omit<
|
|
12
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
13
|
+
"value" | "defaultValue" | "onChange" | "min" | "max" | "step"
|
|
14
|
+
> {
|
|
15
|
+
/** Controlled value (in the value domain — a detent value when `detents` is set). */
|
|
16
|
+
value?: number;
|
|
17
|
+
/** Uncontrolled initial value. Defaults to the first detent, or `min`. */
|
|
18
|
+
defaultValue?: number;
|
|
19
|
+
/** Called with the new value on every change. */
|
|
20
|
+
onChange?: (value: number) => void;
|
|
21
|
+
/** Minimum (continuous mode). Default: 0 */
|
|
22
|
+
min?: number;
|
|
23
|
+
/** Maximum (continuous mode). Default: 100 */
|
|
24
|
+
max?: number;
|
|
25
|
+
/** Step granularity (continuous mode). Default: 1 */
|
|
26
|
+
step?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Optional snap stops. Pass numbers, or `{ value, label }` to show labels under
|
|
29
|
+
* the track (e.g. `[{value:0,label:'None'},{value:1,label:'XS'},…]`). The thumb
|
|
30
|
+
* snaps between detents and the keyboard moves one detent at a time.
|
|
31
|
+
*/
|
|
32
|
+
detents?: SliderDetent[];
|
|
33
|
+
/**
|
|
34
|
+
* Visible field label rendered above the control and associated with it (also
|
|
35
|
+
* the accessible name). Sized to match the field family per `size`. Use
|
|
36
|
+
* `aria-label` / `aria-labelledby` instead for an invisible name.
|
|
37
|
+
*/
|
|
38
|
+
label?: React.ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Density, matching the field family. Default: "default". Scales the label,
|
|
41
|
+
* detent labels, track, and thumb so a Slider sits naturally beside fields.
|
|
42
|
+
*/
|
|
43
|
+
size?: "compact" | "default" | "comfortable";
|
|
44
|
+
/**
|
|
45
|
+
* Selection colour. "default" uses the action colour; "subtle" uses neutrals
|
|
46
|
+
* for the fill, thumb, and active detent. Default: "default"
|
|
47
|
+
*/
|
|
48
|
+
variant?: "default" | "subtle";
|
|
49
|
+
/** Show the floating value bubble while dragging/focused. Default: true */
|
|
50
|
+
showValue?: boolean;
|
|
51
|
+
/** Preferred bubble side; it flips to stay in the viewport. Default: "above" */
|
|
52
|
+
valuePosition?: "above" | "below";
|
|
53
|
+
/** Format the bubble + `aria-valuetext`. Defaults to the detent label or the number. */
|
|
54
|
+
formatValue?: (value: number) => React.ReactNode;
|
|
55
|
+
/**
|
|
56
|
+
* An alternate label shown in the value bubble (visual only — `aria-valuetext`
|
|
57
|
+
* is unchanged). A node is used as-is; a function receives the current value
|
|
58
|
+
* and the active detent. When omitted, the bubble keeps its current content
|
|
59
|
+
* (the formatted value, detent label/icon, or the raw value). Useful for a
|
|
60
|
+
* longer spoken-out size name (e.g. "Small") while the detent stays "SM".
|
|
61
|
+
*/
|
|
62
|
+
bubbleLabel?: React.ReactNode | ((value: number, detent: { value: number; label?: React.ReactNode; icon?: string } | null) => React.ReactNode);
|
|
63
|
+
/** Disable the slider. Default: false */
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
/** Form field name. */
|
|
66
|
+
name?: string;
|
|
67
|
+
id?: string;
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export declare function Slider(props: SliderProps): React.ReactElement;
|