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

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 (111) hide show
  1. package/guidelines/Guidelines.md +228 -0
  2. package/package.json +4 -1
  3. package/src/breakpoints.css +29 -0
  4. package/src/color-scheme.css +586 -24
  5. package/src/components/accordion/Accordion.jsx +80 -0
  6. package/src/components/accordion/accordion.css +118 -0
  7. package/src/components/banner/Banner.jsx +66 -0
  8. package/src/components/banner/banner.css +205 -0
  9. package/src/components/bleed/Bleed.jsx +27 -0
  10. package/src/components/bleed/bleed.css +5 -0
  11. package/src/components/blockquote/Blockquote.jsx +40 -0
  12. package/src/components/blockquote/blockquote.css +166 -0
  13. package/src/components/breadcrumb/Breadcrumb.jsx +82 -0
  14. package/src/components/breadcrumb/breadcrumb.css +133 -0
  15. package/src/components/button/button.css +42 -12
  16. package/src/components/button-container/ButtonContainer.jsx +20 -1
  17. package/src/components/button-container/button-container.css +19 -1
  18. package/src/components/calendar/Calendar.jsx +383 -0
  19. package/src/components/calendar/calendar.css +225 -0
  20. package/src/components/card/Card.jsx +50 -12
  21. package/src/components/card/card.css +178 -14
  22. package/src/components/checkbox-group/CheckboxGroup.jsx +120 -0
  23. package/src/components/checkbox-group/checkbox-group.css +304 -0
  24. package/src/components/cluster/Cluster.jsx +52 -0
  25. package/src/components/cluster/cluster.css +9 -0
  26. package/src/components/code/Code.jsx +135 -0
  27. package/src/components/code/code.css +60 -0
  28. package/src/components/data-table/DataTable.jsx +721 -0
  29. package/src/components/data-table/DataTableFilters.jsx +339 -0
  30. package/src/components/data-table/data-table-filters.css +259 -0
  31. package/src/components/data-table/data-table.css +425 -0
  32. package/src/components/dialog/Dialog.jsx +45 -2
  33. package/src/components/dialog/dialog.css +13 -4
  34. package/src/components/divider/Divider.jsx +64 -0
  35. package/src/components/divider/divider.css +170 -0
  36. package/src/components/field/CreditCardField.jsx +131 -0
  37. package/src/components/field/DateField.jsx +11 -0
  38. package/src/components/field/NumberField.jsx +11 -0
  39. package/src/components/field/PhoneField.jsx +107 -0
  40. package/src/components/field/SelectField.jsx +86 -0
  41. package/src/components/field/TextField.jsx +83 -0
  42. package/src/components/field/TextareaField.jsx +147 -0
  43. package/src/components/field/TimeField.jsx +11 -0
  44. package/src/components/field/ZipField.jsx +114 -0
  45. package/src/components/field/credit-card.css +30 -0
  46. package/src/components/field/field.css +380 -0
  47. package/src/components/field/textarea-field.css +185 -0
  48. package/src/components/field-row/FieldRow.jsx +23 -0
  49. package/src/components/field-row/field-row.css +51 -0
  50. package/src/components/fieldset/Fieldset.jsx +49 -0
  51. package/src/components/fieldset/fieldset.css +75 -0
  52. package/src/components/figure/Figure.jsx +63 -0
  53. package/src/components/figure/figure.css +97 -0
  54. package/src/components/grid/Grid.jsx +36 -2
  55. package/src/components/grid/grid.css +129 -4
  56. package/src/components/heading/Heading.jsx +41 -1
  57. package/src/components/heading/heading.css +65 -4
  58. package/src/components/icon/icon.css +1 -0
  59. package/src/components/icon-button/icon-button.css +1 -0
  60. package/src/components/inline/inline.css +51 -0
  61. package/src/components/inline-editable/InlineEditable.jsx +77 -0
  62. package/src/components/inline-editable/inline-editable.css +47 -0
  63. package/src/components/inset/Inset.jsx +27 -0
  64. package/src/components/inset/inset.css +6 -0
  65. package/src/components/labels/Labels.jsx +5 -5
  66. package/src/components/link/Link.jsx +2 -3
  67. package/src/components/link/link.css +30 -1
  68. package/src/components/list/List.jsx +92 -0
  69. package/src/components/list/list.css +178 -0
  70. package/src/components/menu/Menu.jsx +243 -10
  71. package/src/components/menu/menu.css +157 -17
  72. package/src/components/message/Message.jsx +25 -50
  73. package/src/components/message/message.css +50 -33
  74. package/src/components/notification/Notification.jsx +1 -1
  75. package/src/components/page-layout/PageLayout.jsx +16 -1
  76. package/src/components/page-layout/page-layout.css +97 -4
  77. package/src/components/page-nav/PageNav.jsx +110 -0
  78. package/src/components/page-nav/page-nav.css +167 -0
  79. package/src/components/paragraph/Paragraph.jsx +35 -2
  80. package/src/components/paragraph/paragraph.css +38 -1
  81. package/src/components/radio-group/RadioGroup.jsx +121 -0
  82. package/src/components/radio-group/radio-group.css +268 -0
  83. package/src/components/section/Section.jsx +108 -0
  84. package/src/components/section/section.css +280 -0
  85. package/src/components/segmented-control/SegmentedControl.jsx +4 -0
  86. package/src/components/segmented-control/segmented.css +13 -0
  87. package/src/components/side-nav/SideNav.jsx +29 -9
  88. package/src/components/side-nav/scrim.css +1 -1
  89. package/src/components/side-nav/side-nav.css +70 -32
  90. package/src/components/snackbar/Snackbar.jsx +56 -0
  91. package/src/components/snackbar/snackbar.css +113 -0
  92. package/src/components/spacer/Spacer.jsx +36 -0
  93. package/src/components/spacer/spacer.css +44 -0
  94. package/src/components/stack/Stack.jsx +100 -0
  95. package/src/components/stack/stack.css +37 -0
  96. package/src/components/switch/Switch.jsx +114 -0
  97. package/src/components/switch/switch.css +276 -0
  98. package/src/components/system-banner/SystemBanner.jsx +57 -0
  99. package/src/components/system-banner/system-banner.css +118 -0
  100. package/src/components/tabs/Tabs.jsx +96 -28
  101. package/src/components/tabs/tabs.css +352 -15
  102. package/src/components/token-select/TokenSelect.jsx +159 -0
  103. package/src/components/token-select/token-select.css +110 -0
  104. package/src/components/top-header/TopHeader.jsx +641 -0
  105. package/src/components/top-header/top-header.css +337 -0
  106. package/src/illustrations/ComponentThumbnails.jsx +227 -0
  107. package/src/index.js +41 -5
  108. package/src/themes.css +256 -5
  109. package/src/tokens.css +919 -0
  110. package/src/utilities/spacing.css +8 -0
  111. package/src/utilities/sr-only.css +16 -0
@@ -0,0 +1,170 @@
1
+ /* ══════════════════════════════════════════════════════════════════════════
2
+ Divider
3
+ ══════════════════════════════════════════════════════════════════════════ */
4
+
5
+ .a1-divider {
6
+ flex: 0 0 auto;
7
+ box-sizing: border-box;
8
+ border: 0;
9
+ color: var(--semantic-color-border-subtle);
10
+
11
+ /* All orientation-dependent properties driven by custom properties.
12
+ Orientation classes (and responsive overrides) set these vars. */
13
+ width: var(--a1-divider-width, 100%);
14
+ height: var(--a1-divider-height, 0);
15
+ min-height: var(--a1-divider-min-height, 0);
16
+ border-block-start: var(--a1-divider-border-bs, none);
17
+ border-inline-start: var(--a1-divider-border-is, none);
18
+ align-self: var(--a1-divider-align-self, auto);
19
+ margin-block: var(--a1-divider-margin-block, 0);
20
+ margin-inline: var(--a1-divider-margin-inline, 0);
21
+ }
22
+
23
+ /* ── Orientation ─────────────────────────────────────────────────────────── */
24
+
25
+ /* Horizontal — also the xs-h responsive class */
26
+ .a1-divider--horizontal,
27
+ .a1-divider--xs-h {
28
+ --a1-divider-width: 100%;
29
+ --a1-divider-height: 0;
30
+ --a1-divider-min-height: 0;
31
+ --a1-divider-border-bs: var(--a1-divider-size) var(--a1-divider-style) currentColor;
32
+ --a1-divider-border-is: 0;
33
+ --a1-divider-align-self: auto;
34
+ --a1-divider-margin-block: var(--a1-divider-space-value, 0);
35
+ --a1-divider-margin-inline: 0;
36
+ }
37
+
38
+ /* Vertical — also the xs-v responsive class */
39
+ .a1-divider--vertical,
40
+ .a1-divider--xs-v {
41
+ --a1-divider-width: 0;
42
+ --a1-divider-height: auto;
43
+ --a1-divider-min-height: var(--base-spacing-24);
44
+ --a1-divider-border-bs: 0;
45
+ --a1-divider-border-is: var(--a1-divider-size) var(--a1-divider-style) currentColor;
46
+ --a1-divider-align-self: stretch;
47
+ --a1-divider-margin-block: 0;
48
+ --a1-divider-margin-inline: var(--a1-divider-space-value, 0);
49
+ }
50
+
51
+ /* ── Responsive orientation ──────────────────────────────────────────────── */
52
+
53
+ @media (--bp-sm-up) {
54
+ .a1-divider--sm-h {
55
+ --a1-divider-width: 100%;
56
+ --a1-divider-height: 0;
57
+ --a1-divider-min-height: 0;
58
+ --a1-divider-border-bs: var(--a1-divider-size) var(--a1-divider-style) currentColor;
59
+ --a1-divider-border-is: 0;
60
+ --a1-divider-align-self: auto;
61
+ --a1-divider-margin-block: var(--a1-divider-space-value, 0);
62
+ --a1-divider-margin-inline: 0;
63
+ }
64
+ .a1-divider--sm-v {
65
+ --a1-divider-width: 0;
66
+ --a1-divider-height: auto;
67
+ --a1-divider-min-height: var(--base-spacing-24);
68
+ --a1-divider-border-bs: 0;
69
+ --a1-divider-border-is: var(--a1-divider-size) var(--a1-divider-style) currentColor;
70
+ --a1-divider-align-self: stretch;
71
+ --a1-divider-margin-block: 0;
72
+ --a1-divider-margin-inline: var(--a1-divider-space-value, 0);
73
+ }
74
+ }
75
+
76
+ @media (--bp-md-up) {
77
+ .a1-divider--md-h {
78
+ --a1-divider-width: 100%;
79
+ --a1-divider-height: 0;
80
+ --a1-divider-min-height: 0;
81
+ --a1-divider-border-bs: var(--a1-divider-size) var(--a1-divider-style) currentColor;
82
+ --a1-divider-border-is: 0;
83
+ --a1-divider-align-self: auto;
84
+ --a1-divider-margin-block: var(--a1-divider-space-value, 0);
85
+ --a1-divider-margin-inline: 0;
86
+ }
87
+ .a1-divider--md-v {
88
+ --a1-divider-width: 0;
89
+ --a1-divider-height: auto;
90
+ --a1-divider-min-height: var(--base-spacing-24);
91
+ --a1-divider-border-bs: 0;
92
+ --a1-divider-border-is: var(--a1-divider-size) var(--a1-divider-style) currentColor;
93
+ --a1-divider-align-self: stretch;
94
+ --a1-divider-margin-block: 0;
95
+ --a1-divider-margin-inline: var(--a1-divider-space-value, 0);
96
+ }
97
+ }
98
+
99
+ @media (--bp-lg-up) {
100
+ .a1-divider--lg-h {
101
+ --a1-divider-width: 100%;
102
+ --a1-divider-height: 0;
103
+ --a1-divider-min-height: 0;
104
+ --a1-divider-border-bs: var(--a1-divider-size) var(--a1-divider-style) currentColor;
105
+ --a1-divider-border-is: 0;
106
+ --a1-divider-align-self: auto;
107
+ --a1-divider-margin-block: var(--a1-divider-space-value, 0);
108
+ --a1-divider-margin-inline: 0;
109
+ }
110
+ .a1-divider--lg-v {
111
+ --a1-divider-width: 0;
112
+ --a1-divider-height: auto;
113
+ --a1-divider-min-height: var(--base-spacing-24);
114
+ --a1-divider-border-bs: 0;
115
+ --a1-divider-border-is: var(--a1-divider-size) var(--a1-divider-style) currentColor;
116
+ --a1-divider-align-self: stretch;
117
+ --a1-divider-margin-block: 0;
118
+ --a1-divider-margin-inline: var(--a1-divider-space-value, 0);
119
+ }
120
+ }
121
+
122
+ @media (--bp-xl) {
123
+ .a1-divider--xl-h {
124
+ --a1-divider-width: 100%;
125
+ --a1-divider-height: 0;
126
+ --a1-divider-min-height: 0;
127
+ --a1-divider-border-bs: var(--a1-divider-size) var(--a1-divider-style) currentColor;
128
+ --a1-divider-border-is: 0;
129
+ --a1-divider-align-self: auto;
130
+ --a1-divider-margin-block: var(--a1-divider-space-value, 0);
131
+ --a1-divider-margin-inline: 0;
132
+ }
133
+ .a1-divider--xl-v {
134
+ --a1-divider-width: 0;
135
+ --a1-divider-height: auto;
136
+ --a1-divider-min-height: var(--base-spacing-24);
137
+ --a1-divider-border-bs: 0;
138
+ --a1-divider-border-is: var(--a1-divider-size) var(--a1-divider-style) currentColor;
139
+ --a1-divider-align-self: stretch;
140
+ --a1-divider-margin-block: 0;
141
+ --a1-divider-margin-inline: var(--a1-divider-space-value, 0);
142
+ }
143
+ }
144
+
145
+ /* ── Thickness ───────────────────────────────────────────────────────────── */
146
+
147
+ .a1-divider--xs { --a1-divider-size: var(--component-divider-size-xs); }
148
+ .a1-divider--sm { --a1-divider-size: var(--component-divider-size-sm); }
149
+ .a1-divider--md { --a1-divider-size: var(--component-divider-size-md); }
150
+ .a1-divider--lg { --a1-divider-size: var(--component-divider-size-lg); }
151
+
152
+ /* ── Variant ─────────────────────────────────────────────────────────────── */
153
+
154
+ .a1-divider--subtle { --a1-divider-style: solid; color: var(--semantic-color-border-subtle); }
155
+ .a1-divider--strong { --a1-divider-style: solid; color: var(--semantic-color-border-strong); }
156
+ .a1-divider--accent { --a1-divider-style: solid; color: var(--semantic-color-text-accent); }
157
+ .a1-divider--dashed { --a1-divider-style: dashed; color: var(--semantic-color-border-subtle); }
158
+ .a1-divider--dotted { --a1-divider-style: dotted; color: var(--semantic-color-border-subtle); }
159
+
160
+ /* ── Space ───────────────────────────────────────────────────────────────── */
161
+ /* --a1-divider-space-value is routed to margin-block or margin-inline
162
+ by the active orientation class. */
163
+
164
+ .a1-divider--space-none { --a1-divider-space-value: 0; }
165
+ .a1-divider--space-xs { --a1-divider-space-value: var(--base-spacing-4); }
166
+ .a1-divider--space-sm { --a1-divider-space-value: var(--base-spacing-8); }
167
+ .a1-divider--space-md { --a1-divider-space-value: var(--base-spacing-16); }
168
+ .a1-divider--space-lg { --a1-divider-space-value: var(--base-spacing-24); }
169
+ .a1-divider--space-xl { --a1-divider-space-value: var(--base-spacing-32); }
170
+ .a1-divider--space-xxl { --a1-divider-space-value: var(--base-spacing-40); }
@@ -0,0 +1,131 @@
1
+ import { useState, useRef, useLayoutEffect } from "react";
2
+ import { TextField } from "./TextField.jsx";
3
+ import { buildDisplay, extractDigits, maskMaxDigits, nextSlotIndex } from "./maskUtils.js";
4
+ import "./credit-card.css";
5
+
6
+ const CARD_TYPES = [
7
+ { type: "amex", label: "Amex", pattern: /^3[47]/, mask: "#### ###### #####" },
8
+ { type: "mastercard", label: "MC", pattern: /^(5[1-5]|2[2-7])/, mask: "#### #### #### ####" },
9
+ { type: "discover", label: "Disc", pattern: /^6(?:011|22|4[4-9]|5)/, mask: "#### #### #### ####" },
10
+ { type: "visa", label: "Visa", pattern: /^4/, mask: "#### #### #### ####" },
11
+ ];
12
+
13
+ const DEFAULT_MASK = "#### #### #### ####";
14
+
15
+ function detectCard(digits) {
16
+ return CARD_TYPES.find(c => c.pattern.test(digits)) ?? null;
17
+ }
18
+
19
+ export function CreditCardField({
20
+ value,
21
+ defaultValue,
22
+ onChange,
23
+ onKeyDown: externalKeyDown,
24
+ onFocus: externalFocus,
25
+ onClick: externalClick,
26
+ className = "",
27
+ ...props
28
+ }) {
29
+ const [digits, setDigits] = useState(() =>
30
+ value != null
31
+ ? extractDigits(String(value)).slice(0, 16)
32
+ : defaultValue != null
33
+ ? extractDigits(String(defaultValue)).slice(0, 16)
34
+ : ""
35
+ );
36
+
37
+ const inputRef = useRef(null);
38
+ const nextCursor = useRef(null);
39
+
40
+ const currentDigits = value != null
41
+ ? extractDigits(String(value)).slice(0, 16)
42
+ : digits;
43
+
44
+ const card = currentDigits.length > 0 ? detectCard(currentDigits) : null;
45
+ const mask = card?.mask ?? DEFAULT_MASK;
46
+ const maxLen = maskMaxDigits(mask);
47
+ const trimmed = currentDigits.slice(0, maxLen);
48
+ const display = buildDisplay(trimmed, mask);
49
+
50
+ useLayoutEffect(() => {
51
+ if (nextCursor.current !== null && inputRef.current) {
52
+ const pos = nextCursor.current;
53
+ inputRef.current.setSelectionRange(pos, pos);
54
+ nextCursor.current = null;
55
+ }
56
+ });
57
+
58
+ function updateDigits(newDigits) {
59
+ const card = newDigits.length > 0 ? detectCard(newDigits) : null;
60
+ const newMask = card?.mask ?? DEFAULT_MASK;
61
+ const clamped = newDigits.slice(0, maskMaxDigits(newMask));
62
+ if (value == null) setDigits(clamped);
63
+ onChange?.({ target: { value: buildDisplay(clamped, newMask) } });
64
+ nextCursor.current = nextSlotIndex(clamped.length, newMask);
65
+ }
66
+
67
+ function handleKeyDown(e) {
68
+ if (e.key >= "0" && e.key <= "9") {
69
+ e.preventDefault();
70
+ const { selectionStart, selectionEnd } = e.target;
71
+ const base = selectionStart !== selectionEnd ? "" : currentDigits.slice(0, maxLen);
72
+ if (base.length < maxLen) updateDigits(base + e.key);
73
+ } else if (e.key === "Backspace" || e.key === "Delete") {
74
+ e.preventDefault();
75
+ const { selectionStart, selectionEnd } = e.target;
76
+ if (selectionStart !== selectionEnd) updateDigits("");
77
+ else if (currentDigits.length > 0) updateDigits(currentDigits.slice(0, -1));
78
+ }
79
+ externalKeyDown?.(e);
80
+ }
81
+
82
+ function handleChange(e) {
83
+ const newDigits = extractDigits(e.target.value).slice(0, 16);
84
+ updateDigits(newDigits);
85
+ }
86
+
87
+ function handleFocus(e) {
88
+ const pos = nextSlotIndex(trimmed.length, mask);
89
+ requestAnimationFrame(() => { inputRef.current?.setSelectionRange(pos, pos); });
90
+ externalFocus?.(e);
91
+ }
92
+
93
+ function handleClick(e) {
94
+ const pos = nextSlotIndex(trimmed.length, mask);
95
+ requestAnimationFrame(() => { inputRef.current?.setSelectionRange(pos, pos); });
96
+ externalClick?.(e);
97
+ }
98
+
99
+ const splitAt = nextSlotIndex(trimmed.length, mask);
100
+ const inputOverlay = (
101
+ <>
102
+ <div className="a1-field__mask-overlay" aria-hidden="true">
103
+ <span className="a1-field__mask-typed">{display.slice(0, splitAt)}</span>
104
+ <span className="a1-field__mask-placeholder">{display.slice(splitAt)}</span>
105
+ </div>
106
+ {card && (
107
+ <span className="a1-credit-card__badge" aria-hidden="true">{card.label}</span>
108
+ )}
109
+ </>
110
+ );
111
+
112
+ const classes = ["a1-credit-card", card && "a1-credit-card--detected", className]
113
+ .filter(Boolean).join(" ").trim();
114
+
115
+ return (
116
+ <TextField
117
+ ref={inputRef}
118
+ type="tel"
119
+ inputMode="numeric"
120
+ autoComplete="cc-number"
121
+ value={display}
122
+ onChange={handleChange}
123
+ onKeyDown={handleKeyDown}
124
+ onFocus={handleFocus}
125
+ onClick={handleClick}
126
+ inputOverlay={inputOverlay}
127
+ className={classes}
128
+ {...props}
129
+ />
130
+ );
131
+ }
@@ -0,0 +1,11 @@
1
+ import { TextField } from "./TextField.jsx";
2
+
3
+ export function DateField({ className = "", ...props }) {
4
+ return (
5
+ <TextField
6
+ type="date"
7
+ className={`a1-field--fit ${className}`.trim()}
8
+ {...props}
9
+ />
10
+ );
11
+ }
@@ -0,0 +1,11 @@
1
+ import { TextField } from "./TextField.jsx";
2
+
3
+ export function NumberField({ className = "", ...props }) {
4
+ return (
5
+ <TextField
6
+ type="number"
7
+ className={className}
8
+ {...props}
9
+ />
10
+ );
11
+ }
@@ -0,0 +1,107 @@
1
+ import { useState, useRef, useLayoutEffect } from "react";
2
+ import { TextField } from "./TextField.jsx";
3
+ import { buildDisplay, extractDigits, maskMaxDigits, nextSlotIndex } from "./maskUtils.js";
4
+
5
+ const DEFAULT_MASK = "#-###-###-####";
6
+
7
+ export function PhoneField({
8
+ mask = DEFAULT_MASK,
9
+ value,
10
+ defaultValue,
11
+ onChange,
12
+ onKeyDown: externalKeyDown,
13
+ onFocus: externalFocus,
14
+ onClick: externalClick,
15
+ ...props
16
+ }) {
17
+ const maxLen = maskMaxDigits(mask);
18
+
19
+ const [digits, setDigits] = useState(() =>
20
+ value != null
21
+ ? extractDigits(String(value)).slice(0, maxLen)
22
+ : defaultValue != null
23
+ ? extractDigits(String(defaultValue)).slice(0, maxLen)
24
+ : ""
25
+ );
26
+
27
+ const inputRef = useRef(null);
28
+ const nextCursor = useRef(null);
29
+
30
+ // In controlled mode derive digits from value prop; otherwise use internal state
31
+ const currentDigits = value != null
32
+ ? extractDigits(String(value)).slice(0, maxLen)
33
+ : digits;
34
+
35
+ const display = buildDisplay(currentDigits, mask);
36
+
37
+ useLayoutEffect(() => {
38
+ if (nextCursor.current !== null && inputRef.current) {
39
+ const pos = nextCursor.current;
40
+ inputRef.current.setSelectionRange(pos, pos);
41
+ nextCursor.current = null;
42
+ }
43
+ });
44
+
45
+ function updateDigits(newDigits) {
46
+ if (value == null) setDigits(newDigits);
47
+ onChange?.({ target: { value: buildDisplay(newDigits, mask) } });
48
+ nextCursor.current = nextSlotIndex(newDigits.length, mask);
49
+ }
50
+
51
+ function handleKeyDown(e) {
52
+ if (e.key >= "0" && e.key <= "9") {
53
+ e.preventDefault();
54
+ const { selectionStart, selectionEnd } = e.target;
55
+ const base = selectionStart !== selectionEnd ? "" : currentDigits;
56
+ if (base.length < maxLen) updateDigits(base + e.key);
57
+ } else if (e.key === "Backspace" || e.key === "Delete") {
58
+ e.preventDefault();
59
+ const { selectionStart, selectionEnd } = e.target;
60
+ if (selectionStart !== selectionEnd) updateDigits("");
61
+ else if (currentDigits.length > 0) updateDigits(currentDigits.slice(0, -1));
62
+ }
63
+ externalKeyDown?.(e);
64
+ }
65
+
66
+ function handleChange(e) {
67
+ // Handles paste and browser autocomplete
68
+ const newDigits = extractDigits(e.target.value).slice(0, maxLen);
69
+ updateDigits(newDigits);
70
+ }
71
+
72
+ function handleFocus(e) {
73
+ const pos = nextSlotIndex(currentDigits.length, mask);
74
+ requestAnimationFrame(() => { inputRef.current?.setSelectionRange(pos, pos); });
75
+ externalFocus?.(e);
76
+ }
77
+
78
+ function handleClick(e) {
79
+ const pos = nextSlotIndex(currentDigits.length, mask);
80
+ requestAnimationFrame(() => { inputRef.current?.setSelectionRange(pos, pos); });
81
+ externalClick?.(e);
82
+ }
83
+
84
+ const splitAt = nextSlotIndex(currentDigits.length, mask);
85
+ const inputOverlay = (
86
+ <div className="a1-field__mask-overlay" aria-hidden="true">
87
+ <span className="a1-field__mask-typed">{display.slice(0, splitAt)}</span>
88
+ <span className="a1-field__mask-placeholder">{display.slice(splitAt)}</span>
89
+ </div>
90
+ );
91
+
92
+ return (
93
+ <TextField
94
+ ref={inputRef}
95
+ type="tel"
96
+ inputMode="numeric"
97
+ autoComplete="tel"
98
+ value={display}
99
+ onChange={handleChange}
100
+ onKeyDown={handleKeyDown}
101
+ onFocus={handleFocus}
102
+ onClick={handleClick}
103
+ inputOverlay={inputOverlay}
104
+ {...props}
105
+ />
106
+ );
107
+ }
@@ -0,0 +1,86 @@
1
+ import { useId, forwardRef, useContext } from "react";
2
+ import { useLabel } from "../labels/Labels.jsx";
3
+ import { MessageBadge } from "../message/Message.jsx";
4
+ import { Icon } from "../icon/Icon.jsx";
5
+ import { FieldsetContext } from "../fieldset/FieldsetContext.js";
6
+ import "./field.css";
7
+
8
+ const SIZES = ["comfortable", "default", "compact"];
9
+ const LABEL_POSITIONS = ["above", "side"];
10
+
11
+ export const SelectField = forwardRef(function SelectField({
12
+ label,
13
+ hint,
14
+ error,
15
+ size,
16
+ labelPosition,
17
+ required = false,
18
+ disabled = false,
19
+ id: providedId,
20
+ className = "",
21
+ inputOverlay,
22
+ children,
23
+ ...props
24
+ }, ref) {
25
+ const ctx = useContext(FieldsetContext);
26
+ const autoId = useId();
27
+ const id = providedId ?? autoId;
28
+ const hintId = `${id}-hint`;
29
+ const errorId = `${id}-error`;
30
+
31
+ const resolvedSize = SIZES.includes(size) ? size : (ctx?.size ?? "default");
32
+ const resolvedPosition = LABEL_POSITIONS.includes(labelPosition) ? labelPosition : (ctx?.labelPosition ?? "above");
33
+
34
+ const classes = [
35
+ "a1-field",
36
+ `a1-field--${resolvedSize}`,
37
+ `a1-field--label-${resolvedPosition}`,
38
+ error && "a1-field--error",
39
+ required && "a1-field--required",
40
+ disabled && "a1-field--disabled",
41
+ className,
42
+ ].filter(Boolean).join(" ");
43
+
44
+ const describedBy = [error ? errorId : hint ? hintId : null]
45
+ .filter(Boolean).join(" ") || undefined;
46
+
47
+ const requiredText = useLabel("field.required", "Required");
48
+
49
+ return (
50
+ <div className={classes}>
51
+ {label && (
52
+ <label className="a1-field__label" htmlFor={id}>
53
+ {label}
54
+ {required && resolvedSize === "comfortable" ? (
55
+ <MessageBadge status="info" subtle>{requiredText}</MessageBadge>
56
+ ) : required ? (
57
+ <span className="a1-field__asterisk" aria-hidden="true"> *</span>
58
+ ) : null}
59
+ </label>
60
+ )}
61
+ <div className="a1-field__control">
62
+ <select
63
+ ref={ref}
64
+ id={id}
65
+ className="a1-field__select"
66
+ required={required}
67
+ disabled={disabled}
68
+ aria-describedby={describedBy}
69
+ aria-invalid={error ? "true" : undefined}
70
+ {...props}
71
+ >
72
+ {children}
73
+ </select>
74
+ <span className="a1-field__chevron" aria-hidden="true">
75
+ <Icon name="expand_more" />
76
+ </span>
77
+ {inputOverlay}
78
+ </div>
79
+ {error ? (
80
+ <p className="a1-field__message a1-field__message--error" id={errorId} role="alert">{error}</p>
81
+ ) : hint ? (
82
+ <p className="a1-field__message a1-field__message--hint" id={hintId}>{hint}</p>
83
+ ) : null}
84
+ </div>
85
+ );
86
+ });
@@ -0,0 +1,83 @@
1
+ import { useId, forwardRef, 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 "./field.css";
6
+
7
+ const SIZES = ["comfortable", "default", "compact"];
8
+ const LABEL_POSITIONS = ["above", "side"];
9
+
10
+ export const TextField = forwardRef(function TextField({
11
+ label,
12
+ hint,
13
+ error,
14
+ size,
15
+ labelPosition,
16
+ required = false,
17
+ disabled = false,
18
+ readOnly = false,
19
+ id: providedId,
20
+ className = "",
21
+ placeholder: _removed,
22
+ inputOverlay,
23
+ ...props
24
+ }, ref) {
25
+ const ctx = useContext(FieldsetContext);
26
+ const autoId = useId();
27
+ const id = providedId ?? autoId;
28
+ const hintId = `${id}-hint`;
29
+ const errorId = `${id}-error`;
30
+
31
+ const resolvedSize = SIZES.includes(size) ? size : (ctx?.size ?? "default");
32
+ const resolvedPosition = LABEL_POSITIONS.includes(labelPosition) ? labelPosition : (ctx?.labelPosition ?? "above");
33
+
34
+ const classes = [
35
+ "a1-field",
36
+ `a1-field--${resolvedSize}`,
37
+ `a1-field--label-${resolvedPosition}`,
38
+ error && "a1-field--error",
39
+ required && "a1-field--required",
40
+ disabled && "a1-field--disabled",
41
+ readOnly && "a1-field--readonly",
42
+ className,
43
+ ].filter(Boolean).join(" ");
44
+
45
+ const describedBy = [error ? errorId : hint ? hintId : null]
46
+ .filter(Boolean).join(" ") || undefined;
47
+
48
+ const requiredText = useLabel("field.required", "Required");
49
+
50
+ return (
51
+ <div className={classes}>
52
+ {label && (
53
+ <label className="a1-field__label" htmlFor={id}>
54
+ {label}
55
+ {required && resolvedSize === "comfortable" ? (
56
+ <MessageBadge status="info" subtle>{requiredText}</MessageBadge>
57
+ ) : required ? (
58
+ <span className="a1-field__asterisk" aria-hidden="true"> *</span>
59
+ ) : null}
60
+ </label>
61
+ )}
62
+ <div className="a1-field__control">
63
+ <input
64
+ ref={ref}
65
+ id={id}
66
+ className="a1-field__input"
67
+ required={required}
68
+ disabled={disabled}
69
+ readOnly={readOnly}
70
+ aria-describedby={describedBy}
71
+ aria-invalid={error ? "true" : undefined}
72
+ {...props}
73
+ />
74
+ {inputOverlay}
75
+ </div>
76
+ {error ? (
77
+ <p className="a1-field__message a1-field__message--error" id={errorId} role="alert">{error}</p>
78
+ ) : hint ? (
79
+ <p className="a1-field__message a1-field__message--hint" id={hintId}>{hint}</p>
80
+ ) : null}
81
+ </div>
82
+ );
83
+ });