@gtivr4/a1-design-system-react 0.19.0 → 0.21.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/src/components/card/Card.d.ts +9 -0
- package/src/components/card/Card.jsx +22 -0
- package/src/components/card/card.css +21 -2
- package/src/components/code/Code.jsx +33 -24
- package/src/components/code/code.css +8 -1
- package/src/components/dialog/Dialog.jsx +25 -0
- package/src/components/tabs/Tabs.d.ts +16 -0
- package/src/components/tabs/Tabs.jsx +35 -7
- package/src/components/tabs/tabs.css +55 -17
- package/src/components/top-header/top-header.css +7 -0
package/package.json
CHANGED
|
@@ -25,6 +25,15 @@ export interface CardProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
25
25
|
* Default: "action"
|
|
26
26
|
*/
|
|
27
27
|
heroColor?: "action" | "neutral" | "info" | "success" | "warn" | "error" | (string & {});
|
|
28
|
+
/** Badge label overlaid on the hero (only renders when `iconDisplay="hero"`). */
|
|
29
|
+
heroBadge?: React.ReactNode;
|
|
30
|
+
/** Status colour of the hero badge. Default: "neutral" */
|
|
31
|
+
heroBadgeStatus?: "neutral" | "info" | "success" | "warn" | "error";
|
|
32
|
+
/** Placement of the hero badge on a 3×3 grid ("{top|middle|bottom}-{start|center|end}"). Default: "top-end" */
|
|
33
|
+
heroBadgePosition?:
|
|
34
|
+
| "top-start" | "top-center" | "top-end"
|
|
35
|
+
| "middle-start" | "middle-center" | "middle-end"
|
|
36
|
+
| "bottom-start" | "bottom-center" | "bottom-end";
|
|
28
37
|
children?: React.ReactNode;
|
|
29
38
|
}
|
|
30
39
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "./card.css";
|
|
2
2
|
import { Icon } from "../icon/Icon.jsx";
|
|
3
|
+
import { MessageBadge } from "../message/Message.jsx";
|
|
3
4
|
|
|
4
5
|
const HERO_COLORS = {
|
|
5
6
|
action: "var(--semantic-color-action-background)",
|
|
@@ -12,6 +13,14 @@ const HERO_COLORS = {
|
|
|
12
13
|
|
|
13
14
|
const VALID_ICON_DISPLAY = ["none", "default", "hero"];
|
|
14
15
|
|
|
16
|
+
// 3×3 placement of a hero badge: "{block}-{inline}" where block ∈ top|middle|bottom
|
|
17
|
+
// and inline ∈ start|center|end.
|
|
18
|
+
const VALID_HERO_BADGE_POSITIONS = [
|
|
19
|
+
"top-start", "top-center", "top-end",
|
|
20
|
+
"middle-start", "middle-center", "middle-end",
|
|
21
|
+
"bottom-start", "bottom-center", "bottom-end",
|
|
22
|
+
];
|
|
23
|
+
|
|
15
24
|
export function Card({
|
|
16
25
|
as,
|
|
17
26
|
bare = false,
|
|
@@ -20,6 +29,9 @@ export function Card({
|
|
|
20
29
|
icon,
|
|
21
30
|
iconDisplay = "default",
|
|
22
31
|
heroColor = "action",
|
|
32
|
+
heroBadge,
|
|
33
|
+
heroBadgeStatus = "neutral",
|
|
34
|
+
heroBadgePosition = "top-end",
|
|
23
35
|
className = "",
|
|
24
36
|
children,
|
|
25
37
|
...props
|
|
@@ -47,12 +59,22 @@ export function Card({
|
|
|
47
59
|
? { type: "button" }
|
|
48
60
|
: {};
|
|
49
61
|
|
|
62
|
+
const badgePos = VALID_HERO_BADGE_POSITIONS.includes(heroBadgePosition)
|
|
63
|
+
? heroBadgePosition
|
|
64
|
+
: "top-end";
|
|
65
|
+
const [badgeBlock, badgeInline] = badgePos.split("-");
|
|
66
|
+
|
|
50
67
|
return (
|
|
51
68
|
<Component className={classes} href={href} {...interactiveProps} {...props}>
|
|
52
69
|
<div className="a1-card__layout">
|
|
53
70
|
{resolvedDisplay === "hero" && (
|
|
54
71
|
<div className="a1-card__hero" style={{ "--a1-card-hero-bg": heroBg }}>
|
|
55
72
|
<Icon name={icon} aria-hidden="true" />
|
|
73
|
+
{heroBadge && (
|
|
74
|
+
<span className={`a1-card__hero-badge a1-card__hero-badge--${badgeBlock} a1-card__hero-badge--${badgeInline}`}>
|
|
75
|
+
<MessageBadge status={heroBadgeStatus} size="sm">{heroBadge}</MessageBadge>
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
56
78
|
</div>
|
|
57
79
|
)}
|
|
58
80
|
{resolvedDisplay === "default" && (
|
|
@@ -79,6 +79,7 @@ button.a1-card--navigation {
|
|
|
79
79
|
|
|
80
80
|
.a1-card__hero {
|
|
81
81
|
/* Bleed out to the card edges on all four sides then add inner padding */
|
|
82
|
+
position: relative;
|
|
82
83
|
margin-top: calc(-1 * var(--component-card-padding));
|
|
83
84
|
margin-inline: calc(-1 * var(--component-card-padding));
|
|
84
85
|
margin-bottom: var(--component-card-padding);
|
|
@@ -91,8 +92,26 @@ button.a1-card--navigation {
|
|
|
91
92
|
--a1-icon-opsz: 48;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/* Hero badge — overlaid on the hero, placed via a 3×3 grid. */
|
|
96
|
+
.a1-card__hero-badge {
|
|
97
|
+
position: absolute;
|
|
98
|
+
z-index: 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.a1-card__hero-badge--top { inset-block-start: var(--base-spacing-8); }
|
|
102
|
+
.a1-card__hero-badge--bottom { inset-block-end: var(--base-spacing-8); }
|
|
103
|
+
.a1-card__hero-badge--middle { inset-block-start: 50%; }
|
|
104
|
+
.a1-card__hero-badge--start { inset-inline-start: var(--base-spacing-8); }
|
|
105
|
+
.a1-card__hero-badge--end { inset-inline-end: var(--base-spacing-8); }
|
|
106
|
+
.a1-card__hero-badge--center { inset-inline-start: 50%; }
|
|
107
|
+
|
|
108
|
+
/* Centre transforms for the middle/centre axes (combine when both). */
|
|
109
|
+
.a1-card__hero-badge--middle:not(.a1-card__hero-badge--center) { transform: translateY(-50%); }
|
|
110
|
+
.a1-card__hero-badge--center:not(.a1-card__hero-badge--middle) { transform: translateX(-50%); }
|
|
111
|
+
.a1-card__hero-badge--middle.a1-card__hero-badge--center { transform: translate(-50%, -50%); }
|
|
112
|
+
|
|
94
113
|
/* Higher specificity (0,2,0) beats .a1-icon (0,1,0) so font-size is not overridden by inherit */
|
|
95
|
-
.a1-card__hero .a1-icon {
|
|
114
|
+
.a1-card__hero > .a1-icon {
|
|
96
115
|
font-size: var(--base-spacing-64);
|
|
97
116
|
color: var(--semantic-color-text-inverse);
|
|
98
117
|
}
|
|
@@ -120,7 +139,7 @@ button.a1-card--navigation {
|
|
|
120
139
|
border-end-end-radius: 0;
|
|
121
140
|
}
|
|
122
141
|
|
|
123
|
-
.a1-card__hero .a1-icon {
|
|
142
|
+
.a1-card__hero > .a1-icon {
|
|
124
143
|
font-size: var(--base-spacing-128);
|
|
125
144
|
}
|
|
126
145
|
|
|
@@ -146,7 +146,11 @@ export function Code({
|
|
|
146
146
|
);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
// Cap the height whenever collapsible + not expanded (so the overflow check has
|
|
150
|
+
// a clamped height to measure against); `clipped` adds the fade only when the
|
|
151
|
+
// content actually overflows the cap.
|
|
152
|
+
const collapsed = collapses && !expanded;
|
|
153
|
+
const clipped = collapsed && overflows;
|
|
150
154
|
|
|
151
155
|
return (
|
|
152
156
|
<div
|
|
@@ -155,6 +159,7 @@ export function Code({
|
|
|
155
159
|
copyCode && "a1-code-block--copyable",
|
|
156
160
|
editable && "a1-code-block--editable",
|
|
157
161
|
collapsed && "a1-code-block--collapsed",
|
|
162
|
+
clipped && "a1-code-block--clipped",
|
|
158
163
|
className,
|
|
159
164
|
]
|
|
160
165
|
.filter(Boolean)
|
|
@@ -185,29 +190,33 @@ export function Code({
|
|
|
185
190
|
</code>
|
|
186
191
|
</pre>
|
|
187
192
|
)}
|
|
188
|
-
{copyCode && (
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
193
|
+
{(copyCode || (collapses && overflows)) && (
|
|
194
|
+
<div className="a1-code-block__actions">
|
|
195
|
+
{copyCode && (
|
|
196
|
+
<Button
|
|
197
|
+
className="a1-code-block__copy"
|
|
198
|
+
icon="content_copy"
|
|
199
|
+
size="sm"
|
|
200
|
+
variant="tertiary"
|
|
201
|
+
onClick={handleCopy}
|
|
202
|
+
type="button"
|
|
203
|
+
>
|
|
204
|
+
{copied ? copiedLabel : copyLabel}
|
|
205
|
+
</Button>
|
|
206
|
+
)}
|
|
207
|
+
{collapses && overflows && (
|
|
208
|
+
<Button
|
|
209
|
+
className="a1-code-block__toggle"
|
|
210
|
+
icon={expanded ? "expand_less" : "expand_more"}
|
|
211
|
+
size="sm"
|
|
212
|
+
variant="tertiary"
|
|
213
|
+
onClick={() => setExpanded((v) => !v)}
|
|
214
|
+
type="button"
|
|
215
|
+
>
|
|
216
|
+
{expanded ? showLessLabel : showMoreLabel}
|
|
217
|
+
</Button>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
211
220
|
)}
|
|
212
221
|
</div>
|
|
213
222
|
);
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
overflow-y: hidden;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
.a1-code-block--
|
|
98
|
+
.a1-code-block--clipped .a1-code-block__pre::after {
|
|
99
99
|
content: "";
|
|
100
100
|
position: absolute;
|
|
101
101
|
inset-inline: 0;
|
|
@@ -110,3 +110,10 @@
|
|
|
110
110
|
.a1-code-block__toggle {
|
|
111
111
|
margin: 0;
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
/* Copy + Show more/less sit inline on one row. */
|
|
115
|
+
.a1-code-block__actions {
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
gap: var(--base-spacing-8);
|
|
119
|
+
}
|
|
@@ -67,6 +67,31 @@ export function Dialog({
|
|
|
67
67
|
return () => el.removeEventListener("cancel", handleCancel);
|
|
68
68
|
}, [onClose]);
|
|
69
69
|
|
|
70
|
+
// Dismissable dialogs (those with an onClose) also close when the backdrop — the
|
|
71
|
+
// area outside the dialog box — is clicked, matching Escape. Require both the press
|
|
72
|
+
// and the release to land on the backdrop so a drag that starts inside the dialog
|
|
73
|
+
// (e.g. selecting text and releasing past the edge) doesn't dismiss it.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const el = ref.current;
|
|
76
|
+
if (!open || !el || !onClose) return;
|
|
77
|
+
const onBackdrop = (e) => {
|
|
78
|
+
const r = el.getBoundingClientRect();
|
|
79
|
+
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
|
|
80
|
+
};
|
|
81
|
+
let downOnBackdrop = false;
|
|
82
|
+
const handleMouseDown = (e) => { downOnBackdrop = e.target === el && onBackdrop(e); };
|
|
83
|
+
const handleClick = (e) => {
|
|
84
|
+
if (downOnBackdrop && e.target === el && onBackdrop(e)) onClose();
|
|
85
|
+
downOnBackdrop = false;
|
|
86
|
+
};
|
|
87
|
+
el.addEventListener("mousedown", handleMouseDown);
|
|
88
|
+
el.addEventListener("click", handleClick);
|
|
89
|
+
return () => {
|
|
90
|
+
el.removeEventListener("mousedown", handleMouseDown);
|
|
91
|
+
el.removeEventListener("click", handleClick);
|
|
92
|
+
};
|
|
93
|
+
}, [open, onClose]);
|
|
94
|
+
|
|
70
95
|
useEffect(() => {
|
|
71
96
|
const el = ref.current;
|
|
72
97
|
if (!open || !el) return;
|
|
@@ -21,6 +21,22 @@ export interface TabsProps {
|
|
|
21
21
|
level?: 1 | 2;
|
|
22
22
|
/** Size variant. Default: undefined (standard) */
|
|
23
23
|
size?: "compact";
|
|
24
|
+
/**
|
|
25
|
+
* Keep every panel mounted, stacked in one cell, so the panel area always reserves
|
|
26
|
+
* the **tallest** panel's height — switching tabs won't change the container height.
|
|
27
|
+
* Opt-in (default `false`); use inside a Dialog/overlay so it doesn't resize and move
|
|
28
|
+
* its targets. On a page, leave it off so tabs size to the active panel.
|
|
29
|
+
* Default: false
|
|
30
|
+
*/
|
|
31
|
+
equalHeight?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Label display. `"all"` (default) shows every tab's label at all breakpoints.
|
|
34
|
+
* `"selected"` shows the label only on the **active** tab; inactive tabs render
|
|
35
|
+
* **icon-only** (the label stays in the DOM for the accessible name). Pair with a
|
|
36
|
+
* `Tab` `icon` so inactive tabs aren't blank, and give **every** `Tab` a label.
|
|
37
|
+
* Mirrors `ToolbarGroup`'s `labelMode`. Default: "all"
|
|
38
|
+
*/
|
|
39
|
+
labelMode?: "all" | "selected";
|
|
24
40
|
className?: string;
|
|
25
41
|
children?: React.ReactNode;
|
|
26
42
|
}
|
|
@@ -6,11 +6,11 @@ const TabsContext = createContext(null);
|
|
|
6
6
|
|
|
7
7
|
/* ─── Tabs ─────────────────────────────────────────────────────────────────── */
|
|
8
8
|
|
|
9
|
-
export function Tabs({ children, value, onChange, variant = "line", level = 1, size, className = "" }) {
|
|
9
|
+
export function Tabs({ children, value, onChange, variant = "line", level = 1, size, equalHeight = false, labelMode = "all", className = "" }) {
|
|
10
10
|
const uid = useId();
|
|
11
11
|
return (
|
|
12
|
-
<TabsContext.Provider value={{ value, onChange, variant, level, size, uid }}>
|
|
13
|
-
<div className={["a1-tabs", `a1-tabs--level-${level}`, size && `a1-tabs--${size}`, className].filter(Boolean).join(" ")}>
|
|
12
|
+
<TabsContext.Provider value={{ value, onChange, variant, level, size, uid, equalHeight, labelMode }}>
|
|
13
|
+
<div className={["a1-tabs", `a1-tabs--level-${level}`, size && `a1-tabs--${size}`, equalHeight && "a1-tabs--equal-height", labelMode === "selected" && "a1-tabs--label-selected", className].filter(Boolean).join(" ")}>
|
|
14
14
|
{children}
|
|
15
15
|
</div>
|
|
16
16
|
</TabsContext.Provider>
|
|
@@ -20,7 +20,7 @@ export function Tabs({ children, value, onChange, variant = "line", level = 1, s
|
|
|
20
20
|
/* ─── TabList ───────────────────────────────────────────────────────────────── */
|
|
21
21
|
|
|
22
22
|
export function TabList({ children }) {
|
|
23
|
-
const { variant } = useContext(TabsContext);
|
|
23
|
+
const { variant, value } = useContext(TabsContext);
|
|
24
24
|
const scrollRef = useRef(null);
|
|
25
25
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
26
26
|
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
@@ -49,6 +49,24 @@ export function TabList({ children }) {
|
|
|
49
49
|
};
|
|
50
50
|
}, [checkScroll, enableScroll]);
|
|
51
51
|
|
|
52
|
+
// Keep the selected tab in view when it changes (and on mount) by adjusting only
|
|
53
|
+
// the strip's own horizontal scroll — never scrollIntoView, which would also move
|
|
54
|
+
// the page vertically.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enableScroll) return;
|
|
57
|
+
const el = scrollRef.current;
|
|
58
|
+
const active = el?.querySelector('[role="tab"][aria-selected="true"]');
|
|
59
|
+
if (!el || !active) return;
|
|
60
|
+
const pad = 16;
|
|
61
|
+
const elRect = el.getBoundingClientRect();
|
|
62
|
+
const aRect = active.getBoundingClientRect();
|
|
63
|
+
if (aRect.left < elRect.left) {
|
|
64
|
+
el.scrollBy({ left: aRect.left - elRect.left - pad, behavior: "smooth" });
|
|
65
|
+
} else if (aRect.right > elRect.right) {
|
|
66
|
+
el.scrollBy({ left: aRect.right - elRect.right + pad, behavior: "smooth" });
|
|
67
|
+
}
|
|
68
|
+
}, [value, enableScroll]);
|
|
69
|
+
|
|
52
70
|
const scrollBy = (dir) => {
|
|
53
71
|
scrollRef.current?.scrollBy({ left: dir * 200, behavior: "smooth" });
|
|
54
72
|
};
|
|
@@ -154,15 +172,25 @@ export function Tab({ children, value: tabValue, count, icon, iconPosition = "st
|
|
|
154
172
|
/* ─── TabPanel ──────────────────────────────────────────────────────────────── */
|
|
155
173
|
|
|
156
174
|
export function TabPanel({ children, value: panelValue }) {
|
|
157
|
-
const { value, variant, uid } = useContext(TabsContext);
|
|
158
|
-
|
|
175
|
+
const { value, variant, uid, equalHeight } = useContext(TabsContext);
|
|
176
|
+
const isActive = value === panelValue;
|
|
177
|
+
// Default: unmount inactive panels (the panel area sizes to the active tab). With
|
|
178
|
+
// equalHeight, keep every panel mounted but stacked + hidden, so the area always
|
|
179
|
+
// reserves the tallest panel's height and the container never resizes on switch.
|
|
180
|
+
if (!equalHeight && !isActive) return null;
|
|
159
181
|
|
|
160
182
|
return (
|
|
161
183
|
<div
|
|
162
184
|
role="tabpanel"
|
|
163
185
|
id={`${uid}-panel-${panelValue}`}
|
|
164
186
|
aria-labelledby={`${uid}-tab-${panelValue}`}
|
|
165
|
-
|
|
187
|
+
aria-hidden={equalHeight && !isActive ? true : undefined}
|
|
188
|
+
className={[
|
|
189
|
+
"a1-tab-panel",
|
|
190
|
+
`a1-tab-panel--${variant}`,
|
|
191
|
+
equalHeight && "a1-tab-panel--stacked",
|
|
192
|
+
equalHeight && !isActive && "a1-tab-panel--inactive",
|
|
193
|
+
].filter(Boolean).join(" ")}
|
|
166
194
|
>
|
|
167
195
|
{children}
|
|
168
196
|
</div>
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
box-sizing: border-box;
|
|
5
5
|
display: flex;
|
|
6
6
|
flex-direction: column;
|
|
7
|
+
/* Never force a flex/grid parent wider than its share: allow shrinking and cap
|
|
8
|
+
at the parent width so an overflowing tab strip scrolls instead of pushing it. */
|
|
9
|
+
min-width: 0;
|
|
10
|
+
max-width: 100%;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
/* ─── Tab list wrapper ────────────────────────────────────────────────────── */
|
|
@@ -17,6 +21,8 @@
|
|
|
17
21
|
.a1-tab-list-wrapper {
|
|
18
22
|
display: flex;
|
|
19
23
|
align-items: stretch;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
max-width: 100%;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
.a1-tab-list-wrapper--line {
|
|
@@ -63,6 +69,11 @@
|
|
|
63
69
|
/* Line variant gets overflow scroll; the tab list scrolls, wrapper border stays full-width */
|
|
64
70
|
.a1-tab-list--scrollable {
|
|
65
71
|
flex: 1;
|
|
72
|
+
/* min-width: 0 lets this flex item shrink below the tabs' intrinsic (min-content)
|
|
73
|
+
width — without it the strip grows to fit every tab and forces a horizontal
|
|
74
|
+
page/container overflow instead of scrolling internally. This is what makes the
|
|
75
|
+
overflow-x scroll (and the scroll-arrow buttons) actually engage. */
|
|
76
|
+
min-width: 0;
|
|
66
77
|
overflow-x: auto;
|
|
67
78
|
/* Setting overflow-x alone makes the browser compute overflow-y to `auto`,
|
|
68
79
|
which lets a sub-pixel of height add a stray vertical scroll. Pin it. */
|
|
@@ -478,24 +489,23 @@
|
|
|
478
489
|
border-bottom-left-radius: var(--base-radius-control);
|
|
479
490
|
}
|
|
480
491
|
|
|
481
|
-
/* ───
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
492
|
+
/* ─── Label on selected only (opt-in via labelMode="selected") ─────────────────
|
|
493
|
+
* Inactive tabs render icon-only; the selected tab shows its label. The label stays
|
|
494
|
+
* in the DOM (visually hidden) so the accessible name is preserved. Pair with a Tab
|
|
495
|
+
* `icon` so inactive tabs aren't blank, and give every Tab a label. */
|
|
496
|
+
.a1-tabs--label-selected .a1-tab {
|
|
497
|
+
position: relative;
|
|
498
|
+
}
|
|
487
499
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
white-space: nowrap;
|
|
498
|
-
}
|
|
500
|
+
.a1-tabs--label-selected .a1-tab[aria-selected="false"] .a1-tab__label {
|
|
501
|
+
position: absolute;
|
|
502
|
+
width: 1px;
|
|
503
|
+
height: 1px;
|
|
504
|
+
padding: 0;
|
|
505
|
+
margin: -1px;
|
|
506
|
+
overflow: hidden;
|
|
507
|
+
clip: rect(0, 0, 0, 0);
|
|
508
|
+
white-space: nowrap;
|
|
499
509
|
}
|
|
500
510
|
|
|
501
511
|
/* ─── Responsive font sizes ───────────────────────────────────────────────── */
|
|
@@ -545,6 +555,34 @@
|
|
|
545
555
|
padding: var(--base-spacing-24) 0;
|
|
546
556
|
}
|
|
547
557
|
|
|
558
|
+
/* ─── Equal-height panels (opt-in) ──────────────────────────────────────────────
|
|
559
|
+
* Keep every panel mounted and stacked in one grid cell so the panel area always
|
|
560
|
+
* reserves the *tallest* panel's height — switching tabs no longer changes the
|
|
561
|
+
* container height. Use inside a Dialog/overlay so the dialog doesn't resize and
|
|
562
|
+
* move its targets. Inactive panels stay laid out (so they contribute height) but
|
|
563
|
+
* are visually hidden and removed from the a11y tree (aria-hidden), which also
|
|
564
|
+
* takes their focusable descendants out of the tab order.
|
|
565
|
+
*/
|
|
566
|
+
.a1-tabs--equal-height {
|
|
567
|
+
display: grid;
|
|
568
|
+
grid-template-columns: minmax(0, 1fr);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.a1-tabs--equal-height > .a1-tab-list-wrapper {
|
|
572
|
+
grid-row: 1;
|
|
573
|
+
grid-column: 1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.a1-tab-panel--stacked {
|
|
577
|
+
grid-row: 2;
|
|
578
|
+
grid-column: 1;
|
|
579
|
+
min-width: 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.a1-tab-panel--inactive {
|
|
583
|
+
visibility: hidden;
|
|
584
|
+
}
|
|
585
|
+
|
|
548
586
|
/* ─── Compact size ──────────────────────────────────────────────────────────── */
|
|
549
587
|
|
|
550
588
|
.a1-tabs--compact .a1-tab {
|
|
@@ -213,6 +213,13 @@
|
|
|
213
213
|
color: var(--semantic-color-text-muted);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/* When the parent is the current/selected item (a child page is active), its
|
|
217
|
+
leading icon matches the selected text colour instead of staying muted. */
|
|
218
|
+
.a1-top-header__flyout-trigger.a1-menu-item--active .a1-top-header__flyout-icon,
|
|
219
|
+
.a1-top-header__flyout-trigger[aria-current="page"] .a1-top-header__flyout-icon {
|
|
220
|
+
color: currentColor;
|
|
221
|
+
}
|
|
222
|
+
|
|
216
223
|
.a1-top-header__flyout-chevron {
|
|
217
224
|
margin-inline-start: auto;
|
|
218
225
|
flex-shrink: 0;
|