@gtivr4/a1-design-system-react 0.7.0 → 0.8.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.
@@ -158,7 +158,6 @@ Every named export from `@gtivr4/a1-design-system-react` — verified against `s
158
158
  | `MessageEmptyState` | Empty state block; `scale="page\|section\|card"`, `icon`, `title`, `description`, `action` |
159
159
  | `Notification` | Badge wrapper; `count`, `label`, `dot`, `variant`, `position`, `max` |
160
160
  | `Snackbar` | Toast notification; `open`, `onClose`, `message`, `action` |
161
- | `SystemBanner` | Full-width system alert; `status`, `title`, `message`, `onDismiss` |
162
161
 
163
162
  ### Overlay
164
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtivr4/a1-design-system-react",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "React components for the A1 token-driven design system.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -14,7 +14,7 @@ export interface CircularProgressProps {
14
14
  * Circle diameter. xs renders the smallest ring (no inner content — children
15
15
  * are placed inline after the ring instead). Default: "md"
16
16
  */
17
- size?: "xs" | "sm" | "md" | "lg";
17
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
18
18
  /**
19
19
  * Shows a continuously rotating arc instead of a value-based fill.
20
20
  * Removes aria-valuenow so assistive technology announces an indeterminate
@@ -1,6 +1,6 @@
1
1
  import "./circular-progress.css";
2
2
 
3
- const SIZES = ["xs", "sm", "md", "lg"];
3
+ const SIZES = ["xs", "sm", "md", "lg", "xl"];
4
4
 
5
5
  // SVG uses a 100×100 viewBox with a 10-unit stroke so the ring scales
6
6
  // proportionally when the container size changes via CSS.
@@ -17,6 +17,7 @@
17
17
  .a1-circular-progress--sm { --a1-cp-size: var(--component-circular-progress-sm-size); }
18
18
  /* md is the default — no modifier needed */
19
19
  .a1-circular-progress--lg { --a1-cp-size: var(--component-circular-progress-lg-size); }
20
+ .a1-circular-progress--xl { --a1-cp-size: var(--component-circular-progress-xl-size); }
20
21
 
21
22
  /* ─── Ring (SVG + optional inner content) ────────────────────────────────────── */
22
23
 
@@ -391,7 +391,7 @@
391
391
  padding-inline-start: var(--base-spacing-4);
392
392
  padding-inline-end: var(--a1-field-padding-inline);
393
393
  font-family: var(--component-paragraph-font-family);
394
- font-size: var(--semantic-font-size-body-xs);
394
+ font-size: var(--a1-field-font-size);
395
395
  color: var(--semantic-color-text-muted);
396
396
  line-height: 1;
397
397
  pointer-events: auto;
@@ -8,6 +8,28 @@ import "./top-header.css";
8
8
 
9
9
  // ── Helpers ────────────────────────────────────────────────────────────────────
10
10
 
11
+ // Breakpoint min-widths that match the system breakpoint tokens.
12
+ const BP_QUERIES = {
13
+ sm: "(min-width: 481px)",
14
+ md: "(min-width: 641px)",
15
+ lg: "(min-width: 1025px)",
16
+ xl: "(min-width: 1441px)",
17
+ };
18
+
19
+ // Resolve a scalar or responsive { xs?, sm?, md?, lg?, xl? } navIconPosition
20
+ // value to a boolean indicating whether icon-above mode is active right now.
21
+ function resolveIconAbove(prop) {
22
+ if (!prop || typeof prop === "string") return prop === "above";
23
+ // Cascade xs → sm → md → lg → xl, carrying forward the last explicit value.
24
+ let resolved = prop.xs ?? "start";
25
+ for (const [bp, query] of Object.entries(BP_QUERIES)) {
26
+ if (typeof window !== "undefined" && window.matchMedia(query).matches) {
27
+ resolved = prop[bp] ?? resolved;
28
+ }
29
+ }
30
+ return resolved === "above";
31
+ }
32
+
11
33
  // Split a flat items array into sections separated by { divider: true } markers.
12
34
  function splitIntoSections(items) {
13
35
  const sections = [];
@@ -211,7 +233,7 @@ function NavMenuItem({ item, onClose }) {
211
233
 
212
234
  // ── NavItem (desktop) ──────────────────────────────────────────────────────────
213
235
 
214
- function NavItem({ item, openId, onOpen }) {
236
+ function NavItem({ item, openId, onOpen, iconAbove }) {
215
237
  const triggerRef = useRef(null);
216
238
  const hasSubmenu = item.items?.length > 0;
217
239
  const hasRoute = !!item.href;
@@ -234,7 +256,32 @@ function NavItem({ item, openId, onOpen }) {
234
256
  .filter(Boolean)
235
257
  .join(" ");
236
258
 
237
- const linkContent = (
259
+ // In icon-above mode the chevron is nested inside the label span so it sits
260
+ // inline with the text in the column layout, rather than appearing as a
261
+ // third stacked row below the icon and label.
262
+ const linkContent = iconAbove ? (
263
+ <>
264
+ {item.icon && (
265
+ <Icon
266
+ name={item.icon}
267
+ className="a1-top-header__nav-link-icon"
268
+ aria-hidden="true"
269
+ />
270
+ )}
271
+ {!isIconOnly && (
272
+ <span className="a1-top-header__nav-link-label">
273
+ {item.label}
274
+ {hasSubmenu && (
275
+ <Icon
276
+ name="expand_more"
277
+ className="a1-top-header__nav-chevron"
278
+ aria-hidden="true"
279
+ />
280
+ )}
281
+ </span>
282
+ )}
283
+ </>
284
+ ) : (
238
285
  <>
239
286
  {item.icon && (
240
287
  <Icon
@@ -247,14 +294,13 @@ function NavItem({ item, openId, onOpen }) {
247
294
  </>
248
295
  );
249
296
 
250
- const submenuChevron = (
251
- hasSubmenu && (
252
- <Icon
253
- name="expand_more"
254
- className="a1-top-header__nav-chevron"
255
- aria-hidden="true"
256
- />
257
- )
297
+ // In icon-above mode the chevron lives inside linkContent (see above).
298
+ const submenuChevron = !iconAbove && hasSubmenu && (
299
+ <Icon
300
+ name="expand_more"
301
+ className="a1-top-header__nav-chevron"
302
+ aria-hidden="true"
303
+ />
258
304
  );
259
305
 
260
306
  const submenuButtonContent = (
@@ -540,8 +586,10 @@ export function TopHeader({
540
586
  navItems = [],
541
587
  actions = [],
542
588
  loginButton,
589
+ navIconPosition = "start",
543
590
  className = "",
544
591
  }) {
592
+ const [iconAbove, setIconAbove] = useState(() => resolveIconAbove(navIconPosition));
545
593
  const [openSubmenu, setOpenSubmenu] = useState(null);
546
594
  const [openAction, setOpenAction] = useState(null);
547
595
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
@@ -569,10 +617,29 @@ export function TopHeader({
569
617
  return () => desktopQuery.removeListener(closeAtDesktop);
570
618
  }, [mobileNavOpen]);
571
619
 
620
+ // Re-resolve iconAbove whenever navIconPosition or the viewport changes.
621
+ useEffect(() => {
622
+ if (typeof window === "undefined" || !window.matchMedia) return undefined;
623
+
624
+ const update = () => setIconAbove(resolveIconAbove(navIconPosition));
625
+ const listeners = Object.values(BP_QUERIES).map((q) => {
626
+ const mq = window.matchMedia(q);
627
+ mq.addEventListener("change", update);
628
+ return [mq, update];
629
+ });
630
+ update();
631
+ return () => listeners.forEach(([mq, fn]) => mq.removeEventListener("change", fn));
632
+ // eslint-disable-next-line react-hooks/exhaustive-deps
633
+ }, [JSON.stringify(navIconPosition)]);
634
+
572
635
  return (
573
636
  <>
574
637
  <header
575
- className={["a1-top-header", className].filter(Boolean).join(" ")}
638
+ className={[
639
+ "a1-top-header",
640
+ iconAbove && "a1-top-header--nav-icon-above",
641
+ className,
642
+ ].filter(Boolean).join(" ")}
576
643
  >
577
644
  <button
578
645
  type="button"
@@ -599,6 +666,7 @@ export function TopHeader({
599
666
  item={item}
600
667
  openId={openSubmenu}
601
668
  onOpen={setOpenSubmenu}
669
+ iconAbove={iconAbove}
602
670
  />
603
671
  ))}
604
672
  </ul>
@@ -320,6 +320,38 @@
320
320
  margin-inline-start: var(--base-spacing-4);
321
321
  }
322
322
 
323
+ /* ── Icon-above nav layout ────────────────────────────────────────────────── */
324
+
325
+ /* Nav links become a column: icon above, label (+ optional chevron) below. */
326
+ .a1-top-header--nav-icon-above .a1-top-header__nav-link {
327
+ flex-direction: column;
328
+ align-items: center;
329
+ gap: var(--base-spacing-2);
330
+ padding-block: var(--base-spacing-8);
331
+ padding-inline: var(--base-spacing-8);
332
+ font-size: var(--semantic-font-size-body-xs);
333
+ }
334
+
335
+ /* Icon slightly larger so it reads clearly at the top of each item. */
336
+ .a1-top-header--nav-icon-above .a1-top-header__nav-link-icon {
337
+ font-size: var(--semantic-font-size-heading-sm);
338
+ }
339
+
340
+ /* Label row: inline-flex so the small chevron sits beside the text. */
341
+ .a1-top-header--nav-icon-above .a1-top-header__nav-link-label {
342
+ display: inline-flex;
343
+ align-items: center;
344
+ gap: var(--base-spacing-2);
345
+ line-height: 1;
346
+ }
347
+
348
+ /* Chevron in this context is a small inline decoration next to the label. */
349
+ .a1-top-header--nav-icon-above .a1-top-header__nav-link-label .a1-top-header__nav-chevron {
350
+ font-size: var(--semantic-font-size-body-sm);
351
+ margin-inline-start: 0;
352
+ transition: transform var(--semantic-motion-duration-fast) var(--semantic-motion-easing-standard);
353
+ }
354
+
323
355
  /* ── Responsive ──────────────────────────────────────────────────────────── */
324
356
 
325
357
  @media (max-width: 768px) {
@@ -334,4 +366,23 @@
334
366
  .a1-top-header__nav {
335
367
  display: none;
336
368
  }
369
+
370
+ /* Icon-above mode: nav is always visible; hamburger is not needed. */
371
+ .a1-top-header--nav-icon-above .a1-top-header__hamburger {
372
+ display: none;
373
+ }
374
+
375
+ .a1-top-header--nav-icon-above .a1-top-header__nav {
376
+ display: flex;
377
+ }
378
+
379
+ /* Allow horizontal scrolling if items overflow on very small screens. */
380
+ .a1-top-header--nav-icon-above .a1-top-header__nav-list {
381
+ overflow-x: auto;
382
+ scrollbar-width: none;
383
+ }
384
+
385
+ .a1-top-header--nav-icon-above .a1-top-header__nav-list::-webkit-scrollbar {
386
+ display: none;
387
+ }
337
388
  }
package/src/tokens.css CHANGED
@@ -130,6 +130,7 @@
130
130
  --base-spacing-64: 4rem;
131
131
  --base-spacing-96: 6rem;
132
132
  --base-spacing-128: 8rem;
133
+ --base-spacing-192: 12rem;
133
134
  --base-spacing-neg-4: -0.25rem;
134
135
  --base-spacing-neg-8: -0.5rem;
135
136
  --base-spacing-neg-12: -0.75rem;
@@ -414,6 +415,7 @@
414
415
  --component-circular-progress-sm-size: 4rem;
415
416
  --component-circular-progress-md-size: 6rem;
416
417
  --component-circular-progress-lg-size: 8rem;
418
+ --component-circular-progress-xl-size: 12rem;
417
419
  --component-circular-progress-track-color: #e1e8f3;
418
420
  --component-circular-progress-fill-color: #7c3aed;
419
421
  --component-circular-progress-gap: 0.5rem;
@@ -1,17 +0,0 @@
1
- import * as React from "react";
2
-
3
- export interface SystemBannerProps {
4
- /** Semantic status colour. Default: "neutral" */
5
- status?: "neutral" | "info" | "success" | "warn" | "error";
6
- /** Bold title text */
7
- title?: string;
8
- /** Override the default status icon with any Material Symbols name */
9
- icon?: string;
10
- /** Action element rendered at the trailing end */
11
- action?: React.ReactNode;
12
- /** Called when the dismiss button is clicked. Omit to hide the dismiss button. */
13
- onDismiss?: () => void;
14
- children?: React.ReactNode;
15
- }
16
-
17
- export declare function SystemBanner(props: SystemBannerProps): React.ReactElement;
@@ -1,57 +0,0 @@
1
- import "./system-banner.css";
2
- import { Icon } from "../icon/Icon.jsx";
3
- import { IconButton } from "../icon-button/IconButton.jsx";
4
-
5
- const STATUS_ICONS = {
6
- neutral: "campaign",
7
- info: "info",
8
- success: "check_circle",
9
- warn: "warning",
10
- error: "error",
11
- };
12
-
13
- const STATUSES = ["neutral", "info", "success", "warn", "error"];
14
-
15
- export function SystemBanner({
16
- status = "neutral",
17
- title,
18
- icon,
19
- action,
20
- onDismiss,
21
- children,
22
- }) {
23
- const resolvedStatus = STATUSES.includes(status) ? status : "neutral";
24
- const resolvedIcon = icon ?? STATUS_ICONS[resolvedStatus];
25
-
26
- return (
27
- <div
28
- className={`a1-system-banner a1-system-banner--${resolvedStatus}`}
29
- role="alert"
30
- aria-live="polite"
31
- >
32
- <div className="a1-system-banner__inner">
33
- <span className="a1-system-banner__icon" aria-hidden="true">
34
- <Icon name={resolvedIcon} />
35
- </span>
36
-
37
- <div className="a1-system-banner__content">
38
- {title && <span className="a1-system-banner__title">{title}</span>}
39
- {children && <span className="a1-system-banner__body">{children}</span>}
40
- </div>
41
-
42
- {action && (
43
- <div className="a1-system-banner__action">{action}</div>
44
- )}
45
-
46
- {onDismiss && (
47
- <IconButton
48
- icon="close"
49
- label="Dismiss"
50
- onClick={onDismiss}
51
- className="a1-system-banner__dismiss"
52
- />
53
- )}
54
- </div>
55
- </div>
56
- );
57
- }
@@ -1,118 +0,0 @@
1
- /* ─── System Banner ────────────────────────────────────────────────────────── */
2
-
3
- .a1-system-banner {
4
- --a1-sysbanner-bg: var(--semantic-color-surface-inverse);
5
- --a1-sysbanner-fg: var(--semantic-color-text-inverse);
6
-
7
- background: var(--a1-sysbanner-bg);
8
- color: var(--a1-sysbanner-fg);
9
- width: 100%;
10
- }
11
-
12
- .a1-system-banner--info { --a1-sysbanner-bg: var(--semantic-color-status-info-background); }
13
- .a1-system-banner--success { --a1-sysbanner-bg: var(--semantic-color-status-success-background); }
14
- .a1-system-banner--warn { --a1-sysbanner-bg: var(--semantic-color-status-warn-background); }
15
- .a1-system-banner--error { --a1-sysbanner-bg: var(--semantic-color-status-error-background); }
16
-
17
- /* ─── Inner layout ─────────────────────────────────────────────────────────── */
18
-
19
- .a1-system-banner__inner {
20
- display: flex;
21
- align-items: center;
22
- flex-wrap: wrap;
23
- gap: var(--base-spacing-8) var(--base-spacing-12);
24
- max-width: var(--component-message-banner-system-max-width);
25
- margin-inline: auto;
26
- padding-block: var(--base-spacing-12);
27
- padding-inline: var(--base-spacing-24);
28
- }
29
-
30
- /* ─── Icon ─────────────────────────────────────────────────────────────────── */
31
-
32
- .a1-system-banner__icon {
33
- flex-shrink: 0;
34
- display: flex;
35
- font-size: var(--component-message-banner-icon-size);
36
- line-height: 1;
37
- --a1-icon-opsz: var(--component-message-banner-icon-optical-size);
38
- }
39
-
40
- /* ─── Content (title + body inline) ───────────────────────────────────────── */
41
-
42
- .a1-system-banner__content {
43
- flex: 1;
44
- min-width: 0;
45
- display: flex;
46
- flex-wrap: wrap;
47
- align-items: baseline;
48
- gap: 0 var(--base-spacing-8);
49
- font-family: var(--component-paragraph-font-family);
50
- font-size: var(--semantic-font-size-body-sm);
51
- line-height: var(--semantic-font-line-height-body);
52
- }
53
-
54
- .a1-system-banner__title {
55
- font-weight: var(--component-message-banner-title-font-weight);
56
- color: var(--a1-sysbanner-fg);
57
- }
58
-
59
- .a1-system-banner__body {
60
- font-weight: var(--semantic-font-weight-body);
61
- color: var(--a1-sysbanner-fg);
62
- opacity: 0.85;
63
- }
64
-
65
- /* ─── Action ───────────────────────────────────────────────────────────────── */
66
-
67
- .a1-system-banner__action {
68
- flex-shrink: 0;
69
- }
70
-
71
- /* ─── Dismiss ──────────────────────────────────────────────────────────────── */
72
-
73
- .a1-system-banner__dismiss {
74
- flex-shrink: 0;
75
- margin-inline-start: var(--base-spacing-4);
76
- }
77
-
78
- /* ─── Link override on solid bg ────────────────────────────────────────────── */
79
-
80
- .a1-system-banner .a1-link {
81
- color: var(--a1-sysbanner-fg);
82
- }
83
-
84
- .a1-system-banner .a1-link:hover {
85
- color: color-mix(in srgb, var(--a1-sysbanner-fg) 80%, transparent);
86
- }
87
-
88
- .a1-system-banner .a1-link:active {
89
- color: color-mix(in srgb, var(--a1-sysbanner-fg) 65%, transparent);
90
- }
91
-
92
- /* ─── Tertiary button override on solid bg ─────────────────────────────────── */
93
-
94
- .a1-system-banner .a1-button--tertiary {
95
- --a1-button-foreground: var(--a1-sysbanner-fg);
96
- --a1-button-foreground-hover: var(--a1-sysbanner-fg);
97
- --a1-button-foreground-pressed: var(--a1-sysbanner-fg);
98
- --a1-button-background: transparent;
99
- --a1-button-background-hover: color-mix(in srgb, var(--a1-sysbanner-fg) 12%, transparent);
100
- --a1-button-background-pressed: color-mix(in srgb, var(--a1-sysbanner-fg) 20%, transparent);
101
- --a1-button-border: color-mix(in srgb, var(--a1-sysbanner-fg) 40%, transparent);
102
- --a1-button-border-hover: color-mix(in srgb, var(--a1-sysbanner-fg) 60%, transparent);
103
- --a1-button-border-pressed: color-mix(in srgb, var(--a1-sysbanner-fg) 75%, transparent);
104
- }
105
-
106
- /* ─── Icon button (dismiss) override on solid bg ───────────────────────────── */
107
-
108
- .a1-system-banner .a1-icon-button {
109
- --a1-icon-button-foreground: var(--a1-sysbanner-fg);
110
- --a1-icon-button-foreground-hover: var(--a1-sysbanner-fg);
111
- --a1-icon-button-foreground-pressed: var(--a1-sysbanner-fg);
112
- --a1-icon-button-background: transparent;
113
- --a1-icon-button-background-hover: color-mix(in srgb, var(--a1-sysbanner-fg) 12%, transparent);
114
- --a1-icon-button-background-pressed: color-mix(in srgb, var(--a1-sysbanner-fg) 20%, transparent);
115
- --a1-icon-button-border: transparent;
116
- --a1-icon-button-border-hover: transparent;
117
- --a1-icon-button-border-pressed: transparent;
118
- }