@gtivr4/a1-design-system-react 0.18.0 → 0.20.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.d.ts +4 -0
- package/src/components/code/Code.jsx +59 -12
- package/src/components/code/code.css +30 -0
- package/src/components/grid/Grid.d.ts +2 -0
- package/src/components/grid/Grid.jsx +8 -0
- package/src/components/grid/grid.css +6 -0
- 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
|
|
|
@@ -13,6 +13,10 @@ export interface CodeProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
13
13
|
editable?: boolean;
|
|
14
14
|
/** Called with the current string value whenever the editable textarea changes. */
|
|
15
15
|
onChangeValue?: (value: string) => void;
|
|
16
|
+
/** Cap a long read-only block to `collapsedLines` with a fade + Show more/less toggle (the toggle appears only when the content overflows). Block, non-editable only. Default: false */
|
|
17
|
+
collapsible?: boolean;
|
|
18
|
+
/** Approximate number of lines shown when collapsed. Default: 14 */
|
|
19
|
+
collapsedLines?: number;
|
|
16
20
|
children?: React.ReactNode;
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -56,12 +56,17 @@ export function Code({
|
|
|
56
56
|
copyText,
|
|
57
57
|
editable = false,
|
|
58
58
|
onChangeValue,
|
|
59
|
+
collapsible = false,
|
|
60
|
+
collapsedLines = 14,
|
|
59
61
|
className = "",
|
|
60
62
|
children,
|
|
61
63
|
...props
|
|
62
64
|
}) {
|
|
63
65
|
const resolvedVariant = variants.includes(variant) ? variant : "inline";
|
|
64
66
|
const [copied, setCopied] = useState(false);
|
|
67
|
+
const [expanded, setExpanded] = useState(false);
|
|
68
|
+
const [overflows, setOverflows] = useState(false);
|
|
69
|
+
const preRef = useRef(null);
|
|
65
70
|
const [editableValue, setEditableValue] = useState(() =>
|
|
66
71
|
textFromChildren(Children.toArray(children))
|
|
67
72
|
);
|
|
@@ -78,11 +83,15 @@ export function Code({
|
|
|
78
83
|
const copyLabel = useLabel("code.copyCode", "Copy code");
|
|
79
84
|
const copiedLabel = useLabel("code.copied", "Copied");
|
|
80
85
|
const editLabel = useLabel("code.editCode", "Edit code");
|
|
86
|
+
const showMoreLabel = useLabel("code.showMore", "Show more");
|
|
87
|
+
const showLessLabel = useLabel("code.showLess", "Show less");
|
|
81
88
|
const textToCopy = useMemo(
|
|
82
89
|
() => copyText || (editable ? editableValue : textFromChildren(Children.toArray(children))),
|
|
83
90
|
[children, copyText, editable, editableValue],
|
|
84
91
|
);
|
|
85
92
|
const shouldRenderBlock = resolvedVariant === "block" || copyCode || editable;
|
|
93
|
+
// Collapsible only applies to a read-only block (not the editable textarea).
|
|
94
|
+
const collapses = collapsible && !editable && shouldRenderBlock;
|
|
86
95
|
|
|
87
96
|
useEffect(() => {
|
|
88
97
|
return () => {
|
|
@@ -90,6 +99,16 @@ export function Code({
|
|
|
90
99
|
};
|
|
91
100
|
}, []);
|
|
92
101
|
|
|
102
|
+
// Detect whether the (collapsed) content actually overflows the cap, so the
|
|
103
|
+
// toggle only appears when it's needed. scrollHeight reports the full content
|
|
104
|
+
// height even while clipped, so this is accurate in either state.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!collapses) { setOverflows(false); return; }
|
|
107
|
+
if (expanded) return; // measured while collapsed; keep so "Show less" stays
|
|
108
|
+
const el = preRef.current;
|
|
109
|
+
if (el) setOverflows(el.scrollHeight - el.clientHeight > 4);
|
|
110
|
+
}, [collapses, expanded, children, collapsedLines]);
|
|
111
|
+
|
|
93
112
|
function handleTextareaChange(e) {
|
|
94
113
|
setEditableValue(e.target.value);
|
|
95
114
|
onChangeValue?.(e.target.value);
|
|
@@ -127,12 +146,20 @@ export function Code({
|
|
|
127
146
|
);
|
|
128
147
|
}
|
|
129
148
|
|
|
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;
|
|
154
|
+
|
|
130
155
|
return (
|
|
131
156
|
<div
|
|
132
157
|
className={[
|
|
133
158
|
"a1-code-block",
|
|
134
159
|
copyCode && "a1-code-block--copyable",
|
|
135
160
|
editable && "a1-code-block--editable",
|
|
161
|
+
collapsed && "a1-code-block--collapsed",
|
|
162
|
+
clipped && "a1-code-block--clipped",
|
|
136
163
|
className,
|
|
137
164
|
]
|
|
138
165
|
.filter(Boolean)
|
|
@@ -153,23 +180,43 @@ export function Code({
|
|
|
153
180
|
{...editableProps}
|
|
154
181
|
/>
|
|
155
182
|
) : (
|
|
156
|
-
<pre
|
|
183
|
+
<pre
|
|
184
|
+
ref={preRef}
|
|
185
|
+
className="a1-code-block__pre"
|
|
186
|
+
style={collapses ? { "--a1-code-collapsed-max": `${collapsedLines * 1.6}em` } : undefined}
|
|
187
|
+
>
|
|
157
188
|
<code className={codeClasses} {...props}>
|
|
158
189
|
{children}
|
|
159
190
|
</code>
|
|
160
191
|
</pre>
|
|
161
192
|
)}
|
|
162
|
-
{copyCode && (
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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>
|
|
173
220
|
)}
|
|
174
221
|
</div>
|
|
175
222
|
);
|
|
@@ -87,3 +87,33 @@
|
|
|
87
87
|
.a1-code-block__copy {
|
|
88
88
|
margin: 0;
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/* Collapsible block: cap the height with a fade and an Expand/Collapse toggle. */
|
|
92
|
+
.a1-code-block--collapsed .a1-code-block__pre {
|
|
93
|
+
position: relative;
|
|
94
|
+
max-block-size: var(--a1-code-collapsed-max, 22rem);
|
|
95
|
+
overflow-y: hidden;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.a1-code-block--clipped .a1-code-block__pre::after {
|
|
99
|
+
content: "";
|
|
100
|
+
position: absolute;
|
|
101
|
+
inset-inline: 0;
|
|
102
|
+
inset-block-end: 0;
|
|
103
|
+
block-size: var(--base-spacing-48, 3rem);
|
|
104
|
+
background: linear-gradient(to top, var(--semantic-color-surface-panel), transparent);
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
border-end-start-radius: var(--base-radius-md);
|
|
107
|
+
border-end-end-radius: var(--base-radius-md);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.a1-code-block__toggle {
|
|
111
|
+
margin: 0;
|
|
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
|
+
}
|
|
@@ -20,6 +20,8 @@ export interface GridProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
20
20
|
layout?: "default" | "bento";
|
|
21
21
|
/** CSS value for `grid-auto-rows` */
|
|
22
22
|
autoRows?: string;
|
|
23
|
+
/** Cross-axis (vertical) alignment of items within their row. Omit to inherit the grid default ("stretch" = equal-height items filling the row height). */
|
|
24
|
+
alignItems?: "start" | "center" | "end" | "stretch";
|
|
23
25
|
children?: React.ReactNode;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -11,6 +11,7 @@ const gapSizes = {
|
|
|
11
11
|
};
|
|
12
12
|
const layouts = ["default", "bento"];
|
|
13
13
|
const breakpoints = ["xs", "sm", "md", "lg", "xl"];
|
|
14
|
+
const alignments = ["start", "center", "end", "stretch"];
|
|
14
15
|
|
|
15
16
|
function resolveGap(key) {
|
|
16
17
|
if (key == null) return undefined;
|
|
@@ -29,6 +30,7 @@ export function Grid({
|
|
|
29
30
|
columnGap,
|
|
30
31
|
layout = "default",
|
|
31
32
|
autoRows,
|
|
33
|
+
alignItems,
|
|
32
34
|
className = "",
|
|
33
35
|
children,
|
|
34
36
|
...props
|
|
@@ -40,6 +42,12 @@ export function Grid({
|
|
|
40
42
|
classes.push(`a1-grid--${resolvedLayout}`);
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
// Cross-axis (vertical) alignment of items in their row. Omit to inherit the
|
|
46
|
+
// grid default (stretch = equal-height items filling the row).
|
|
47
|
+
if (alignments.includes(alignItems)) {
|
|
48
|
+
classes.push(`a1-grid--align-${alignItems}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
let inlineCols;
|
|
44
52
|
if (typeof columns === "number") {
|
|
45
53
|
inlineCols = columns;
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
align-items: stretch;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/* Cross-axis (vertical) alignment of items within their row. */
|
|
13
|
+
.a1-grid--align-start { align-items: start; }
|
|
14
|
+
.a1-grid--align-center { align-items: center; }
|
|
15
|
+
.a1-grid--align-end { align-items: end; }
|
|
16
|
+
.a1-grid--align-stretch { align-items: stretch; }
|
|
17
|
+
|
|
12
18
|
.a1-grid--bento > .a1-grid-item {
|
|
13
19
|
min-height: 0;
|
|
14
20
|
}
|
|
@@ -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;
|