@gtivr4/a1-design-system-react 0.19.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtivr4/a1-design-system-react",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "React components for the A1 token-driven design system.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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
- const collapsed = collapses && overflows && !expanded;
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
- <Button
190
- className="a1-code-block__copy"
191
- icon="content_copy"
192
- size="sm"
193
- variant="tertiary"
194
- onClick={handleCopy}
195
- type="button"
196
- >
197
- {copied ? copiedLabel : copyLabel}
198
- </Button>
199
- )}
200
- {collapses && overflows && (
201
- <Button
202
- className="a1-code-block__toggle"
203
- icon={expanded ? "expand_less" : "expand_more"}
204
- size="sm"
205
- variant="tertiary"
206
- onClick={() => setExpanded((v) => !v)}
207
- type="button"
208
- >
209
- {expanded ? showLessLabel : showMoreLabel}
210
- </Button>
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--collapsed .a1-code-block__pre::after {
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
+ }
@@ -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;