@gtivr4/a1-design-system-react 0.1.0 → 0.2.3

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.
Files changed (108) hide show
  1. package/package.json +1 -1
  2. package/src/color-scheme.css +586 -24
  3. package/src/components/accordion/Accordion.jsx +80 -0
  4. package/src/components/accordion/accordion.css +118 -0
  5. package/src/components/banner/Banner.jsx +66 -0
  6. package/src/components/banner/banner.css +205 -0
  7. package/src/components/bleed/Bleed.jsx +27 -0
  8. package/src/components/bleed/bleed.css +5 -0
  9. package/src/components/blockquote/Blockquote.jsx +40 -0
  10. package/src/components/blockquote/blockquote.css +166 -0
  11. package/src/components/breadcrumb/Breadcrumb.jsx +82 -0
  12. package/src/components/breadcrumb/breadcrumb.css +133 -0
  13. package/src/components/button/button.css +42 -12
  14. package/src/components/button-container/ButtonContainer.jsx +20 -1
  15. package/src/components/button-container/button-container.css +19 -1
  16. package/src/components/calendar/Calendar.jsx +383 -0
  17. package/src/components/calendar/calendar.css +225 -0
  18. package/src/components/card/Card.jsx +50 -12
  19. package/src/components/card/card.css +178 -14
  20. package/src/components/checkbox-group/CheckboxGroup.jsx +120 -0
  21. package/src/components/checkbox-group/checkbox-group.css +304 -0
  22. package/src/components/cluster/Cluster.jsx +52 -0
  23. package/src/components/cluster/cluster.css +9 -0
  24. package/src/components/code/Code.jsx +135 -0
  25. package/src/components/code/code.css +60 -0
  26. package/src/components/data-table/DataTable.jsx +721 -0
  27. package/src/components/data-table/DataTableFilters.jsx +339 -0
  28. package/src/components/data-table/data-table-filters.css +259 -0
  29. package/src/components/data-table/data-table.css +425 -0
  30. package/src/components/dialog/Dialog.jsx +45 -2
  31. package/src/components/dialog/dialog.css +13 -4
  32. package/src/components/divider/Divider.jsx +64 -0
  33. package/src/components/divider/divider.css +170 -0
  34. package/src/components/field/CreditCardField.jsx +131 -0
  35. package/src/components/field/DateField.jsx +11 -0
  36. package/src/components/field/NumberField.jsx +11 -0
  37. package/src/components/field/PhoneField.jsx +107 -0
  38. package/src/components/field/SelectField.jsx +86 -0
  39. package/src/components/field/TextField.jsx +83 -0
  40. package/src/components/field/TextareaField.jsx +147 -0
  41. package/src/components/field/TimeField.jsx +11 -0
  42. package/src/components/field/ZipField.jsx +114 -0
  43. package/src/components/field/credit-card.css +30 -0
  44. package/src/components/field/field.css +380 -0
  45. package/src/components/field/textarea-field.css +185 -0
  46. package/src/components/field-row/FieldRow.jsx +23 -0
  47. package/src/components/field-row/field-row.css +51 -0
  48. package/src/components/fieldset/Fieldset.jsx +49 -0
  49. package/src/components/fieldset/fieldset.css +75 -0
  50. package/src/components/figure/Figure.jsx +63 -0
  51. package/src/components/figure/figure.css +97 -0
  52. package/src/components/grid/Grid.jsx +36 -2
  53. package/src/components/grid/grid.css +129 -4
  54. package/src/components/heading/Heading.jsx +41 -1
  55. package/src/components/heading/heading.css +65 -4
  56. package/src/components/icon/icon.css +1 -0
  57. package/src/components/icon-button/icon-button.css +1 -0
  58. package/src/components/inline/inline.css +51 -0
  59. package/src/components/inline-editable/InlineEditable.jsx +77 -0
  60. package/src/components/inline-editable/inline-editable.css +47 -0
  61. package/src/components/inset/Inset.jsx +27 -0
  62. package/src/components/inset/inset.css +6 -0
  63. package/src/components/labels/Labels.jsx +5 -5
  64. package/src/components/link/Link.jsx +2 -3
  65. package/src/components/link/link.css +30 -1
  66. package/src/components/list/List.jsx +92 -0
  67. package/src/components/list/list.css +178 -0
  68. package/src/components/menu/Menu.jsx +243 -10
  69. package/src/components/menu/menu.css +157 -17
  70. package/src/components/message/Message.jsx +25 -50
  71. package/src/components/message/message.css +50 -33
  72. package/src/components/notification/Notification.jsx +1 -1
  73. package/src/components/page-layout/PageLayout.jsx +16 -1
  74. package/src/components/page-layout/page-layout.css +97 -4
  75. package/src/components/page-nav/PageNav.jsx +110 -0
  76. package/src/components/page-nav/page-nav.css +167 -0
  77. package/src/components/paragraph/Paragraph.jsx +35 -2
  78. package/src/components/paragraph/paragraph.css +38 -1
  79. package/src/components/radio-group/RadioGroup.jsx +121 -0
  80. package/src/components/radio-group/radio-group.css +268 -0
  81. package/src/components/section/Section.jsx +108 -0
  82. package/src/components/section/section.css +280 -0
  83. package/src/components/segmented-control/SegmentedControl.jsx +4 -0
  84. package/src/components/segmented-control/segmented.css +13 -0
  85. package/src/components/side-nav/SideNav.jsx +29 -9
  86. package/src/components/side-nav/scrim.css +1 -1
  87. package/src/components/side-nav/side-nav.css +70 -32
  88. package/src/components/snackbar/Snackbar.jsx +56 -0
  89. package/src/components/snackbar/snackbar.css +113 -0
  90. package/src/components/spacer/Spacer.jsx +36 -0
  91. package/src/components/spacer/spacer.css +44 -0
  92. package/src/components/stack/Stack.jsx +100 -0
  93. package/src/components/stack/stack.css +37 -0
  94. package/src/components/switch/Switch.jsx +114 -0
  95. package/src/components/switch/switch.css +276 -0
  96. package/src/components/system-banner/SystemBanner.jsx +57 -0
  97. package/src/components/system-banner/system-banner.css +118 -0
  98. package/src/components/tabs/Tabs.jsx +96 -28
  99. package/src/components/tabs/tabs.css +352 -15
  100. package/src/components/token-select/TokenSelect.jsx +159 -0
  101. package/src/components/token-select/token-select.css +110 -0
  102. package/src/components/top-header/TopHeader.jsx +641 -0
  103. package/src/components/top-header/top-header.css +337 -0
  104. package/src/illustrations/ComponentThumbnails.jsx +227 -0
  105. package/src/index.js +41 -5
  106. package/src/themes.css +256 -5
  107. package/src/utilities/spacing.css +8 -0
  108. package/src/utilities/sr-only.css +16 -0
@@ -0,0 +1,110 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Card } from "../card/Card.jsx";
3
+ import "./page-nav.css";
4
+
5
+ export function PageNav({
6
+ sections = [],
7
+ label = "On this page",
8
+ className = "",
9
+ ...props
10
+ }) {
11
+ const [activeId, setActiveId] = useState(sections[0]?.id ?? null);
12
+ const [progress, setProgress] = useState(0);
13
+ const intersectingIds = useRef(new Set());
14
+
15
+ // Reading progress: track document scroll position
16
+ useEffect(() => {
17
+ function update() {
18
+ const el = document.documentElement;
19
+ const total = el.scrollHeight - el.clientHeight;
20
+ setProgress(total > 0 ? (el.scrollTop / total) * 100 : 0);
21
+ }
22
+ window.addEventListener("scroll", update, { passive: true });
23
+ update();
24
+ return () => window.removeEventListener("scroll", update);
25
+ }, []);
26
+
27
+ // Active section: observe each section element entering/leaving the viewport
28
+ useEffect(() => {
29
+ if (!sections.length) return;
30
+
31
+ const observer = new IntersectionObserver(
32
+ (entries) => {
33
+ entries.forEach((entry) => {
34
+ if (entry.isIntersecting) {
35
+ intersectingIds.current.add(entry.target.id);
36
+ } else {
37
+ intersectingIds.current.delete(entry.target.id);
38
+ }
39
+ });
40
+ // Always pick the first match in document order so the active item
41
+ // reflects what the reader is seeing at the top of the viewport.
42
+ const first = sections.find((s) => intersectingIds.current.has(s.id));
43
+ if (first) setActiveId(first.id);
44
+ },
45
+ // -8% top offset keeps the active section stable once it clears the
46
+ // header; -88% bottom offset means only the top 12% of the viewport
47
+ // is treated as "current".
48
+ { rootMargin: "-8% 0px -88% 0px", threshold: 0 }
49
+ );
50
+
51
+ sections.forEach(({ id }) => {
52
+ const el = document.getElementById(id);
53
+ if (el) observer.observe(el);
54
+ });
55
+
56
+ return () => {
57
+ observer.disconnect();
58
+ intersectingIds.current.clear();
59
+ };
60
+ }, [sections]);
61
+
62
+ function handleClick(id) {
63
+ // Immediately highlight the clicked item — don't wait for the observer
64
+ setActiveId(id);
65
+ const el = document.getElementById(id);
66
+ if (!el) return;
67
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
68
+ }
69
+
70
+ return (
71
+ <>
72
+ <Card className={["a1-page-nav", className].filter(Boolean).join(" ")} {...props}>
73
+ {/* Progress bar — spans full card width above padding */}
74
+ <div className="a1-page-nav__progress" aria-hidden="true">
75
+ <div
76
+ className="a1-page-nav__progress-fill"
77
+ style={{ width: `${progress.toFixed(1)}%` }}
78
+ />
79
+ </div>
80
+
81
+ <nav className="a1-page-nav__inner" aria-label={label}>
82
+ <p className="a1-page-nav__heading">{label}</p>
83
+ <ul className="a1-page-nav__list" role="list">
84
+ {sections.map(({ id, label: itemLabel, level = 1 }) => (
85
+ <li key={id} className="a1-page-nav__item">
86
+ <button
87
+ type="button"
88
+ className={[
89
+ "a1-page-nav__link",
90
+ level === 2 && "a1-page-nav__link--l2",
91
+ activeId === id && "a1-page-nav__link--active",
92
+ ]
93
+ .filter(Boolean)
94
+ .join(" ")}
95
+ aria-current={activeId === id ? "location" : undefined}
96
+ onClick={() => handleClick(id)}
97
+ >
98
+ {itemLabel}
99
+ </button>
100
+ </li>
101
+ ))}
102
+ </ul>
103
+ </nav>
104
+ </Card>
105
+
106
+ {/* Reserves space for the fixed pill nav on mobile so content isn't hidden beneath it */}
107
+ <div className="a1-page-nav__spacer" aria-hidden="true" />
108
+ </>
109
+ );
110
+ }
@@ -0,0 +1,167 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════
2
+ PageNav — in-page navigation with reading progress bar
3
+ ═══════════════════════════════════════════════════════════════════════════ */
4
+
5
+ /* ── Card shell override ─────────────────────────────────────────────────── */
6
+
7
+ .a1-page-nav.a1-card {
8
+ padding: 0;
9
+ overflow: hidden; /* clip progress bar to card border-radius */
10
+ }
11
+
12
+ /* ── Progress bar ────────────────────────────────────────────────────────── */
13
+
14
+ .a1-page-nav__progress {
15
+ height: 3px;
16
+ background: var(--semantic-color-border-subtle);
17
+ }
18
+
19
+ .a1-page-nav__progress-fill {
20
+ height: 100%;
21
+ background: var(--semantic-color-action-background);
22
+ min-width: 0;
23
+ transition: width 80ms linear;
24
+ }
25
+
26
+ /* ── Inner layout ────────────────────────────────────────────────────────── */
27
+
28
+ .a1-page-nav__inner {
29
+ padding: var(--base-spacing-16);
30
+ }
31
+
32
+ .a1-page-nav__heading {
33
+ margin: 0 0 var(--base-spacing-12) 0;
34
+ font-family: var(--component-paragraph-font-family);
35
+ font-size: var(--semantic-font-size-body-xs);
36
+ font-weight: 600;
37
+ color: var(--semantic-color-text-muted);
38
+ text-transform: uppercase;
39
+ letter-spacing: 0.08em;
40
+ line-height: 1;
41
+ }
42
+
43
+ .a1-page-nav__list {
44
+ list-style: none;
45
+ margin: 0;
46
+ padding: 0;
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: var(--base-spacing-2);
50
+ }
51
+
52
+ /* ── Nav links ───────────────────────────────────────────────────────────── */
53
+
54
+ .a1-page-nav__link {
55
+ display: block;
56
+ width: 100%;
57
+ text-align: start;
58
+ padding: var(--base-spacing-6) var(--base-spacing-8);
59
+ border: none;
60
+ border-radius: var(--base-radius-md);
61
+ background: transparent;
62
+ cursor: pointer;
63
+ font-family: var(--component-paragraph-font-family);
64
+ font-size: var(--semantic-font-size-body-sm);
65
+ font-weight: 400;
66
+ color: var(--semantic-color-text-muted);
67
+ line-height: 1.4;
68
+ transition:
69
+ color var(--semantic-motion-duration-fast) var(--semantic-motion-easing-standard),
70
+ background var(--semantic-motion-duration-fast) var(--semantic-motion-easing-standard);
71
+ }
72
+
73
+ .a1-page-nav__link:hover {
74
+ color: var(--semantic-color-text-default);
75
+ background: var(--semantic-color-surface-raised);
76
+ }
77
+
78
+ .a1-page-nav__link:focus-visible {
79
+ outline: 2px solid var(--semantic-color-action-background);
80
+ outline-offset: -1px;
81
+ }
82
+
83
+ .a1-page-nav__link--active {
84
+ color: var(--semantic-color-action-background);
85
+ font-weight: 500;
86
+ background: var(--semantic-color-surface-panel);
87
+ }
88
+
89
+ /* Level-2 sub-items (indented, smaller) */
90
+ .a1-page-nav__link--l2 {
91
+ padding-inline-start: var(--base-spacing-20);
92
+ font-size: var(--semantic-font-size-body-xs);
93
+ }
94
+
95
+ /* ── Spacer: hidden on desktop, used only when nav is fixed on mobile ──────── */
96
+
97
+ .a1-page-nav__spacer {
98
+ display: none;
99
+ }
100
+
101
+ /* ── Responsive — fixed pill bar at top of viewport ─────────────────────── */
102
+ /* The card lifts out of the aside and pins to the top of the screen. */
103
+ /* A spacer div below it reserves the equivalent height in the flow. */
104
+
105
+ @media (max-width: 768px) {
106
+ .a1-page-nav.a1-card {
107
+ position: fixed;
108
+ top: 0;
109
+ left: 0;
110
+ right: 0;
111
+ z-index: 200;
112
+ border-radius: 0;
113
+ border-inline: none;
114
+ border-block-start: none;
115
+ /* Crisp shadow to separate from page content scrolling beneath */
116
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.08);
117
+ }
118
+
119
+ /* Spacer height = progress bar (3px) + inner padding (8+8px) + pill row (~35px) */
120
+ .a1-page-nav__spacer {
121
+ display: block;
122
+ height: 54px;
123
+ }
124
+
125
+ .a1-page-nav__heading {
126
+ display: none;
127
+ }
128
+
129
+ .a1-page-nav__inner {
130
+ padding: var(--base-spacing-8) var(--base-spacing-16);
131
+ }
132
+
133
+ .a1-page-nav__list {
134
+ flex-direction: row;
135
+ gap: var(--base-spacing-4);
136
+ overflow-x: auto;
137
+ scrollbar-width: none;
138
+ -webkit-overflow-scrolling: touch;
139
+ padding-block: var(--base-spacing-4);
140
+ }
141
+
142
+ .a1-page-nav__list::-webkit-scrollbar {
143
+ display: none;
144
+ }
145
+
146
+ .a1-page-nav__item {
147
+ flex-shrink: 0;
148
+ }
149
+
150
+ .a1-page-nav__link {
151
+ white-space: nowrap;
152
+ border-radius: 9999px;
153
+ border: 1px solid var(--semantic-color-border-subtle);
154
+ padding: var(--base-spacing-4) var(--base-spacing-12);
155
+ font-size: var(--semantic-font-size-body-xs);
156
+ }
157
+
158
+ .a1-page-nav__link--l2 {
159
+ padding-inline-start: var(--base-spacing-12);
160
+ }
161
+
162
+ .a1-page-nav__link--active {
163
+ background: var(--semantic-color-action-background);
164
+ color: #fff;
165
+ border-color: var(--semantic-color-action-background);
166
+ }
167
+ }
@@ -2,25 +2,58 @@ import "./paragraph.css";
2
2
 
3
3
  const sizes = ["xs", "sm", "md", "lg", "xl"];
4
4
  const colors = ["default", "muted"];
5
+ const breakpoints = ["xs", "sm", "md", "lg", "xl"];
6
+ const textWraps = ["balance"];
7
+ const aligns = ["left", "center", "right"];
8
+
9
+ function isResponsiveSize(size) {
10
+ return size && typeof size === "object" && !Array.isArray(size);
11
+ }
12
+
13
+ function resolveBaseSize(size) {
14
+ if (!isResponsiveSize(size)) return sizes.includes(size) ? size : "md";
15
+ return sizes.includes(size.xs) ? size.xs : "md";
16
+ }
17
+
18
+ function getResponsiveSizeStyle(size) {
19
+ if (!isResponsiveSize(size)) return {};
20
+ return breakpoints.slice(1).reduce((style, bp) => {
21
+ if (sizes.includes(size[bp])) {
22
+ style[`--a1-paragraph-size-${bp}`] = `var(--semantic-font-size-body-${size[bp]})`;
23
+ }
24
+ return style;
25
+ }, {});
26
+ }
5
27
 
6
28
  export function Paragraph({
7
29
  as: Component = "p",
8
30
  size = "md",
9
31
  color = "default",
32
+ textWrap,
33
+ align,
10
34
  className = "",
35
+ style,
11
36
  ...props
12
37
  }) {
13
- const resolvedSize = sizes.includes(size) ? size : "md";
38
+ const resolvedSize = resolveBaseSize(size);
14
39
  const resolvedColor = colors.includes(color) ? color : "default";
40
+ const resolvedTextWrap = textWraps.includes(textWrap) ? textWrap : null;
41
+ const resolvedAlign = aligns.includes(align) ? align : null;
42
+ const responsiveStyle = getResponsiveSizeStyle(size);
43
+ const resolvedStyle = Object.keys(responsiveStyle).length
44
+ ? { ...responsiveStyle, ...style }
45
+ : style;
15
46
 
16
47
  const classes = [
17
48
  "a1-paragraph",
18
49
  `a1-paragraph--${resolvedSize}`,
19
50
  resolvedColor !== "default" && `a1-paragraph--${resolvedColor}`,
51
+ resolvedTextWrap && `a1-paragraph--wrap-${resolvedTextWrap}`,
52
+ resolvedAlign && `a1-paragraph--align-${resolvedAlign}`,
20
53
  className
21
54
  ]
22
55
  .filter(Boolean)
23
56
  .join(" ");
24
57
 
25
- return <Component className={classes} {...props} />;
58
+ return <Component className={classes} style={resolvedStyle} {...props} />;
26
59
  }
@@ -1,12 +1,36 @@
1
1
  .a1-paragraph {
2
2
  margin: 0;
3
3
  font-family: var(--component-paragraph-font-family);
4
- font-size: var(--a1-paragraph-size);
4
+ font-size: var(--a1-paragraph-responsive-size, var(--a1-paragraph-size));
5
5
  font-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
  }
9
9
 
10
+ @media (--bp-sm-up) {
11
+ .a1-paragraph {
12
+ --a1-paragraph-responsive-size: var(--a1-paragraph-size-sm, var(--a1-paragraph-size));
13
+ }
14
+ }
15
+
16
+ @media (--bp-md-up) {
17
+ .a1-paragraph {
18
+ --a1-paragraph-responsive-size: var(--a1-paragraph-size-md, var(--a1-paragraph-size-sm, var(--a1-paragraph-size)));
19
+ }
20
+ }
21
+
22
+ @media (--bp-lg-up) {
23
+ .a1-paragraph {
24
+ --a1-paragraph-responsive-size: var(--a1-paragraph-size-lg, var(--a1-paragraph-size-md, var(--a1-paragraph-size-sm, var(--a1-paragraph-size))));
25
+ }
26
+ }
27
+
28
+ @media (--bp-xl) {
29
+ .a1-paragraph {
30
+ --a1-paragraph-responsive-size: var(--a1-paragraph-size-xl, var(--a1-paragraph-size-lg, var(--a1-paragraph-size-md, var(--a1-paragraph-size-sm, var(--a1-paragraph-size)))));
31
+ }
32
+ }
33
+
10
34
  .a1-paragraph--xs { --a1-paragraph-size: var(--semantic-font-size-body-xs); }
11
35
  .a1-paragraph--sm { --a1-paragraph-size: var(--semantic-font-size-body-sm); }
12
36
  .a1-paragraph--md { --a1-paragraph-size: var(--semantic-font-size-body-md); }
@@ -14,3 +38,16 @@
14
38
  .a1-paragraph--xl { --a1-paragraph-size: var(--semantic-font-size-body-xl); }
15
39
 
16
40
  .a1-paragraph--muted { --a1-paragraph-color: var(--semantic-color-text-muted); }
41
+
42
+ /* Text wrap */
43
+ .a1-paragraph--wrap-balance { text-wrap: balance; }
44
+
45
+ /* Alignment */
46
+ .a1-paragraph--align-left { text-align: start; }
47
+ .a1-paragraph--align-center { text-align: center; }
48
+ .a1-paragraph--align-right { text-align: end; }
49
+
50
+ .a1-paragraph + .a1-paragraph,
51
+ .a1-paragraph + .a1-heading {
52
+ margin-top: 1.5em;
53
+ }
@@ -0,0 +1,121 @@
1
+ import { useId, useState, useContext } from "react";
2
+ import { useLabel } from "../labels/Labels.jsx";
3
+ import { MessageBadge } from "../message/Message.jsx";
4
+ import { FieldsetContext } from "../fieldset/FieldsetContext.js";
5
+ import "./radio-group.css";
6
+
7
+ const SIZES = ["comfortable", "default", "compact"];
8
+
9
+ export function RadioGroup({
10
+ label,
11
+ hint,
12
+ error,
13
+ size,
14
+ required = false,
15
+ disabled = false,
16
+ inline = false,
17
+ name,
18
+ options = [],
19
+ value,
20
+ defaultValue = null,
21
+ onChange,
22
+ id: providedId,
23
+ className = "",
24
+ ...props
25
+ }) {
26
+ const ctx = useContext(FieldsetContext);
27
+ const autoId = useId();
28
+ const id = providedId ?? autoId;
29
+ const hintId = `${id}-hint`;
30
+ const errorId = `${id}-error`;
31
+
32
+ const resolvedSize = SIZES.includes(size) ? size : (ctx?.size ?? "default");
33
+
34
+ const [internalValue, setInternalValue] = useState(defaultValue);
35
+ // value === undefined → uncontrolled; null → controlled with nothing selected
36
+ const currentValue = value !== undefined ? value : internalValue;
37
+
38
+ function handleChange(optionValue) {
39
+ if (value === undefined) setInternalValue(optionValue);
40
+ onChange?.(optionValue);
41
+ }
42
+
43
+ const classes = [
44
+ "a1-radio-group",
45
+ `a1-radio-group--${resolvedSize}`,
46
+ inline && "a1-radio-group--inline",
47
+ error && "a1-radio-group--error",
48
+ required && "a1-radio-group--required",
49
+ disabled && "a1-radio-group--disabled",
50
+ className,
51
+ ].filter(Boolean).join(" ");
52
+
53
+ const describedBy = [error ? errorId : hint ? hintId : null]
54
+ .filter(Boolean).join(" ") || undefined;
55
+
56
+ const requiredText = useLabel("field.required", "Required");
57
+
58
+ // Fall back to the group id so all radios share a name and behave as a group
59
+ const groupName = name ?? id;
60
+
61
+ return (
62
+ <fieldset className={classes} aria-describedby={describedBy} {...props}>
63
+ {label && (
64
+ <legend className="a1-radio-group__legend">
65
+ <span className="a1-radio-group__legend-inner">
66
+ {label}
67
+ {required && resolvedSize === "comfortable" ? (
68
+ <MessageBadge status="info" subtle>{requiredText}</MessageBadge>
69
+ ) : required ? (
70
+ <span className="a1-field__asterisk" aria-hidden="true"> *</span>
71
+ ) : null}
72
+ </span>
73
+ </legend>
74
+ )}
75
+ {hint && !error && (
76
+ <p className="a1-radio-group__message a1-radio-group__message--hint" id={hintId}>
77
+ {hint}
78
+ </p>
79
+ )}
80
+ <div className="a1-radio-group__items">
81
+ {options.map((option) => {
82
+ const isChecked = currentValue === option.value;
83
+ const isDisabled = disabled || option.disabled;
84
+ const itemId = `${id}-${option.value}`;
85
+
86
+ return (
87
+ <label
88
+ key={option.value}
89
+ className={[
90
+ "a1-radio-item",
91
+ isDisabled && "a1-radio-item--disabled",
92
+ ].filter(Boolean).join(" ")}
93
+ >
94
+ <input
95
+ type="radio"
96
+ id={itemId}
97
+ className="a1-radio-item__input"
98
+ name={groupName}
99
+ value={option.value}
100
+ checked={isChecked}
101
+ disabled={isDisabled}
102
+ onChange={() => handleChange(option.value)}
103
+ />
104
+ <span className="a1-radio-item__content">
105
+ <span className="a1-radio-item__label">{option.label}</span>
106
+ {option.hint && (
107
+ <span className="a1-radio-item__hint">{option.hint}</span>
108
+ )}
109
+ </span>
110
+ </label>
111
+ );
112
+ })}
113
+ </div>
114
+ {error && (
115
+ <p className="a1-radio-group__message a1-radio-group__message--error" id={errorId} role="alert">
116
+ {error}
117
+ </p>
118
+ )}
119
+ </fieldset>
120
+ );
121
+ }