@gtivr4/a1-design-system-react 0.6.2 → 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.
- package/guidelines/Guidelines.md +0 -1
- package/package.json +1 -1
- package/src/color-scheme.css +38 -0
- package/src/components/button-container/ButtonContainer.d.ts +2 -0
- package/src/components/button-container/ButtonContainer.jsx +2 -0
- package/src/components/button-container/button-container.css +20 -0
- package/src/components/circular-progress/CircularProgress.d.ts +1 -1
- package/src/components/circular-progress/CircularProgress.jsx +1 -1
- package/src/components/circular-progress/circular-progress.css +1 -0
- package/src/components/data-table/DataTable.d.ts +6 -0
- package/src/components/data-table/DataTable.jsx +49 -33
- package/src/components/data-table/data-table.css +11 -0
- package/src/components/field/NumberField.d.ts +11 -0
- package/src/components/field/NumberField.jsx +55 -3
- package/src/components/field/field.css +131 -0
- package/src/components/icon-button/IconButton.d.ts +2 -0
- package/src/components/icon-button/IconButton.jsx +4 -0
- package/src/components/icon-button/icon-button.css +12 -3
- package/src/components/section/Section.d.ts +1 -1
- package/src/components/section/Section.jsx +1 -1
- package/src/components/section/section.css +3 -1
- package/src/components/stack/Stack.d.ts +1 -1
- package/src/components/stack/Stack.jsx +1 -1
- package/src/components/step-tracker/StepTracker.d.ts +17 -0
- package/src/components/step-tracker/StepTracker.jsx +41 -0
- package/src/components/step-tracker/step-tracker.css +37 -0
- package/src/components/top-header/TopHeader.jsx +79 -11
- package/src/components/top-header/top-header.css +51 -0
- package/src/index.js +1 -0
- package/src/themes.css +2 -4
- package/src/tokens.css +8 -0
- package/src/components/system-banner/SystemBanner.d.ts +0 -17
- package/src/components/system-banner/SystemBanner.jsx +0 -57
- package/src/components/system-banner/system-banner.css +0 -118
package/guidelines/Guidelines.md
CHANGED
|
@@ -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
package/src/color-scheme.css
CHANGED
|
@@ -783,3 +783,41 @@ html.a1-theme-dark.a1-theme-accessible {
|
|
|
783
783
|
--semantic-color-border-subtle: var(--base-color-neutral-500);
|
|
784
784
|
--semantic-color-border-default: var(--base-color-neutral-400);
|
|
785
785
|
}
|
|
786
|
+
|
|
787
|
+
/* ─── Fresh theme — gradient on body, surface tokens on html ─────────────────
|
|
788
|
+
Light mode: mint gradient. Dark mode: dark teal gradient.
|
|
789
|
+
CSS custom properties stay on html so they inherit throughout the document.
|
|
790
|
+
background-image targets body so it doesn't paint behind the html scroll area. */
|
|
791
|
+
|
|
792
|
+
html.a1-theme-fresh body {
|
|
793
|
+
background-image: linear-gradient(207.43deg, #D7FFF8 0%, #CDF5EE 57.44%);
|
|
794
|
+
background-attachment: fixed;
|
|
795
|
+
background-color: var(--semantic-color-surface-page);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/* Dark mode surface tokens — stay on html for inheritance (0,2,1 beats html.a1-theme-dark 0,1,1) */
|
|
799
|
+
html.a1-theme-fresh.a1-theme-dark,
|
|
800
|
+
html.a1-theme-dark.a1-theme-fresh {
|
|
801
|
+
--semantic-color-surface-page: #03453A;
|
|
802
|
+
--semantic-color-surface-panel: #046756;
|
|
803
|
+
--semantic-color-surface-raised: #057868;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
html.a1-theme-fresh.a1-theme-dark body,
|
|
807
|
+
html.a1-theme-dark.a1-theme-fresh body {
|
|
808
|
+
background-image: linear-gradient(207.43deg, #046756 0%, #03453A 57.44%);
|
|
809
|
+
background-color: #03453A;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
@media (prefers-color-scheme: dark) {
|
|
813
|
+
html.a1-theme-fresh {
|
|
814
|
+
--semantic-color-surface-page: #03453A;
|
|
815
|
+
--semantic-color-surface-panel: #046756;
|
|
816
|
+
--semantic-color-surface-raised: #057868;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
html.a1-theme-fresh body {
|
|
820
|
+
background-image: linear-gradient(207.43deg, #046756 0%, #03453A 57.44%);
|
|
821
|
+
background-color: #03453A;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
@@ -5,6 +5,8 @@ export interface ButtonContainerProps extends React.HTMLAttributes<HTMLDivElemen
|
|
|
5
5
|
align?: "start" | "center" | "end";
|
|
6
6
|
/** Default size passed to child Button elements that do not set their own size. */
|
|
7
7
|
size?: "sm" | "md" | "lg";
|
|
8
|
+
/** When true, Button children stretch to fill remaining row space while IconButton children keep their natural square size. Always renders as a row — does not collapse to column on narrow containers. Default: false */
|
|
9
|
+
fillButtons?: boolean;
|
|
8
10
|
children?: React.ReactNode;
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -20,6 +20,7 @@ function applyButtonSize(children, size) {
|
|
|
20
20
|
export function ButtonContainer({
|
|
21
21
|
align = "start",
|
|
22
22
|
size,
|
|
23
|
+
fillButtons = false,
|
|
23
24
|
className = "",
|
|
24
25
|
children,
|
|
25
26
|
...props
|
|
@@ -30,6 +31,7 @@ export function ButtonContainer({
|
|
|
30
31
|
"a1-button-container",
|
|
31
32
|
`a1-button-container--${resolvedAlign}`,
|
|
32
33
|
resolvedSize && `a1-button-container--${resolvedSize}`,
|
|
34
|
+
fillButtons && "a1-button-container--fill-buttons",
|
|
33
35
|
className
|
|
34
36
|
]
|
|
35
37
|
.filter(Boolean)
|
|
@@ -54,3 +54,23 @@
|
|
|
54
54
|
.a1-button-container--lg .a1-button-container__inner {
|
|
55
55
|
gap: var(--base-spacing-16) var(--base-spacing-24);
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/* ── Fill buttons ────────────────────────────────────────────────────────── */
|
|
59
|
+
/* Buttons stretch to fill remaining space; icon buttons stay their natural
|
|
60
|
+
square size. Always row — does not collapse to column on narrow containers. */
|
|
61
|
+
|
|
62
|
+
.a1-button-container--fill-buttons .a1-button-container__inner {
|
|
63
|
+
flex-direction: row;
|
|
64
|
+
flex-wrap: nowrap;
|
|
65
|
+
align-items: center;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.a1-button-container--fill-buttons .a1-button-container__inner > .a1-button {
|
|
69
|
+
flex: 1 1 0;
|
|
70
|
+
min-inline-size: 0;
|
|
71
|
+
width: auto;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.a1-button-container--fill-buttons .a1-button-container__inner > .a1-icon-button {
|
|
75
|
+
flex: 0 0 auto;
|
|
76
|
+
}
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -78,6 +78,12 @@ export interface DataTableProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
78
78
|
emptyTitle?: string;
|
|
79
79
|
emptyDescription?: string;
|
|
80
80
|
emptyIcon?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Custom full-width no-padding rows inserted into the table body.
|
|
83
|
+
* Each entry specifies `content` (ReactNode) and an optional `afterRow` index (0-based,
|
|
84
|
+
* default 0 = before all data rows). Multiple entries with the same `afterRow` stack in order.
|
|
85
|
+
*/
|
|
86
|
+
notices?: Array<{ content: React.ReactNode; afterRow?: number }>;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
export declare function DataTable(props: DataTableProps): React.ReactElement;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
1
|
+
import { Fragment, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Button } from "../button/Button.jsx";
|
|
3
3
|
import { SelectField } from "../field/SelectField.jsx";
|
|
4
4
|
import { Icon } from "../icon/Icon.jsx";
|
|
@@ -197,6 +197,7 @@ export function DataTable({
|
|
|
197
197
|
zebra = false,
|
|
198
198
|
scrollable = false,
|
|
199
199
|
caption,
|
|
200
|
+
notices = [],
|
|
200
201
|
page,
|
|
201
202
|
defaultPage = 1,
|
|
202
203
|
pageSize,
|
|
@@ -661,39 +662,54 @@ export function DataTable({
|
|
|
661
662
|
</tr>
|
|
662
663
|
</thead>
|
|
663
664
|
<tbody>
|
|
664
|
-
{
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
{columns.map((col) => (
|
|
686
|
-
<td
|
|
687
|
-
key={col.key}
|
|
688
|
-
data-label={col.label}
|
|
689
|
-
data-align={getAlign(col)}
|
|
665
|
+
{(() => {
|
|
666
|
+
const noticeColSpan = selectable ? columns.length + 1 : columns.length;
|
|
667
|
+
const noticeMap = {};
|
|
668
|
+
notices.forEach(({ content, afterRow = 0 }) => {
|
|
669
|
+
if (!noticeMap[afterRow]) noticeMap[afterRow] = [];
|
|
670
|
+
noticeMap[afterRow].push(content);
|
|
671
|
+
});
|
|
672
|
+
return visibleRowEntries.map(({ row, index: rowIndex, id: rowId, supportsRowClickSelection }, i) => {
|
|
673
|
+
const isSelected = selectedRowIdSet.has(rowId);
|
|
674
|
+
const noticesHere = noticeMap[i] ?? [];
|
|
675
|
+
return (
|
|
676
|
+
<Fragment key={rowId}>
|
|
677
|
+
{noticesHere.map((content, j) => (
|
|
678
|
+
<tr key={j} className="a1-data-table__notice-row">
|
|
679
|
+
<td className="a1-data-table__notice-cell" colSpan={noticeColSpan}>{content}</td>
|
|
680
|
+
</tr>
|
|
681
|
+
))}
|
|
682
|
+
<tr
|
|
683
|
+
data-selected={isSelected ? "true" : undefined}
|
|
684
|
+
data-selectable-row={supportsRowClickSelection ? "true" : undefined}
|
|
685
|
+
onClick={(event) => handleRowClick(rowId, supportsRowClickSelection, event)}
|
|
690
686
|
>
|
|
691
|
-
{
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
687
|
+
{selectable && (
|
|
688
|
+
<td
|
|
689
|
+
className="a1-data-table__select-cell"
|
|
690
|
+
data-label="Select"
|
|
691
|
+
>
|
|
692
|
+
<SelectionCheckbox
|
|
693
|
+
checked={isSelected}
|
|
694
|
+
label={`Select row ${rowIndex + 1}`}
|
|
695
|
+
onChange={(checked) => toggleRowSelected(rowId, checked)}
|
|
696
|
+
/>
|
|
697
|
+
</td>
|
|
698
|
+
)}
|
|
699
|
+
{columns.map((col) => (
|
|
700
|
+
<td
|
|
701
|
+
key={col.key}
|
|
702
|
+
data-label={col.label}
|
|
703
|
+
data-align={getAlign(col)}
|
|
704
|
+
>
|
|
705
|
+
{renderCell(col, row[col.key])}
|
|
706
|
+
</td>
|
|
707
|
+
))}
|
|
708
|
+
</tr>
|
|
709
|
+
</Fragment>
|
|
710
|
+
);
|
|
711
|
+
});
|
|
712
|
+
})()}
|
|
697
713
|
</tbody>
|
|
698
714
|
</table>
|
|
699
715
|
)}
|
|
@@ -201,6 +201,17 @@
|
|
|
201
201
|
border-bottom: none;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/* ── Notice row (no-padding custom content spanning all columns) ─────────── */
|
|
205
|
+
|
|
206
|
+
/* Double-class specificity (0-2-0) beats density td rules (0-1-1) */
|
|
207
|
+
.a1-data-table .a1-data-table__notice-cell {
|
|
208
|
+
padding: 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.a1-data-table--zebra tbody .a1-data-table__notice-row td {
|
|
212
|
+
background: transparent;
|
|
213
|
+
}
|
|
214
|
+
|
|
204
215
|
/* ── Zebra striping ──────────────────────────────────────────────────────── */
|
|
205
216
|
|
|
206
217
|
.a1-data-table--zebra tbody tr:nth-child(even) td {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { TextFieldProps } from "./TextField.d.ts";
|
|
3
|
+
|
|
4
|
+
export interface NumberFieldProps extends Omit<TextFieldProps, "type"> {
|
|
5
|
+
/** Non-editable prefix rendered before the value at full input size and color (e.g. "$"). */
|
|
6
|
+
prefix?: string;
|
|
7
|
+
/** Non-editable unit label rendered after the value at smaller, muted size (e.g. "lbs", "km", "ft"). */
|
|
8
|
+
unit?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export declare function NumberField(props: NumberFieldProps): React.ReactElement;
|
|
@@ -1,11 +1,63 @@
|
|
|
1
|
+
import { useRef, useCallback, useState, forwardRef } from "react";
|
|
1
2
|
import { TextField } from "./TextField.jsx";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
+
export const NumberField = forwardRef(function NumberField(
|
|
5
|
+
{ unit, prefix, className = "", onChange, value, defaultValue, style, ...props },
|
|
6
|
+
forwardedRef,
|
|
7
|
+
) {
|
|
8
|
+
const localRef = useRef(null);
|
|
9
|
+
|
|
10
|
+
const mergedRef = useCallback((el) => {
|
|
11
|
+
localRef.current = el;
|
|
12
|
+
if (typeof forwardedRef === "function") forwardedRef(el);
|
|
13
|
+
else if (forwardedRef) forwardedRef.current = el;
|
|
14
|
+
}, [forwardedRef]);
|
|
15
|
+
|
|
16
|
+
const [uncontrolledLength, setUncontrolledLength] = useState(
|
|
17
|
+
() => Math.max(String(defaultValue ?? "").length, 1),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const inputLength = value !== undefined
|
|
21
|
+
? Math.max(String(value).length || 1, 1)
|
|
22
|
+
: uncontrolledLength;
|
|
23
|
+
|
|
24
|
+
function handleChange(e) {
|
|
25
|
+
if (value === undefined) {
|
|
26
|
+
setUncontrolledLength(Math.max(e.target.value.length || 1, 1));
|
|
27
|
+
}
|
|
28
|
+
onChange?.(e);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const overlay = (prefix || unit) ? (
|
|
32
|
+
<>
|
|
33
|
+
{prefix && <span className="a1-field__prefix" aria-hidden="true">{prefix}</span>}
|
|
34
|
+
{unit && (
|
|
35
|
+
<span
|
|
36
|
+
className="a1-field__unit"
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
onClick={() => localRef.current?.focus()}
|
|
39
|
+
>
|
|
40
|
+
{unit}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
</>
|
|
44
|
+
) : null;
|
|
45
|
+
|
|
4
46
|
return (
|
|
5
47
|
<TextField
|
|
48
|
+
ref={mergedRef}
|
|
6
49
|
type="number"
|
|
7
|
-
className={
|
|
50
|
+
className={[
|
|
51
|
+
className,
|
|
52
|
+
prefix && "a1-field--has-prefix",
|
|
53
|
+
unit && "a1-field--has-unit",
|
|
54
|
+
].filter(Boolean).join(" ")}
|
|
55
|
+
inputOverlay={overlay}
|
|
56
|
+
onChange={unit ? handleChange : onChange}
|
|
57
|
+
value={value}
|
|
58
|
+
defaultValue={defaultValue}
|
|
59
|
+
style={unit ? { "--a1-field-number-width": `${inputLength}ch`, ...style } : style}
|
|
8
60
|
{...props}
|
|
9
61
|
/>
|
|
10
62
|
);
|
|
11
|
-
}
|
|
63
|
+
});
|
|
@@ -279,6 +279,137 @@
|
|
|
279
279
|
width: auto;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/* ─── Number field prefix (e.g. "$") and unit (e.g. "lbs") ─────────────────── */
|
|
283
|
+
/* The control container becomes the visual field; the input becomes borderless.
|
|
284
|
+
Prefix is reordered before the input via CSS order. Unit follows the input
|
|
285
|
+
as a compact flex child so it sits immediately beside the value. */
|
|
286
|
+
|
|
287
|
+
.a1-field--has-prefix .a1-field__control,
|
|
288
|
+
.a1-field--has-unit .a1-field__control {
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
background: var(--a1-field-background);
|
|
292
|
+
border: var(--component-field-border-width) solid var(--a1-field-border-color);
|
|
293
|
+
border-radius: var(--a1-field-border-radius);
|
|
294
|
+
transition:
|
|
295
|
+
border-color var(--semantic-motion-duration-fast),
|
|
296
|
+
background var(--semantic-motion-duration-fast);
|
|
297
|
+
overflow: hidden;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Input loses its own border/background; container provides them */
|
|
301
|
+
.a1-field--has-prefix .a1-field__input,
|
|
302
|
+
.a1-field--has-unit .a1-field__input {
|
|
303
|
+
border: none;
|
|
304
|
+
background: transparent;
|
|
305
|
+
border-radius: 0;
|
|
306
|
+
outline: none;
|
|
307
|
+
/* suppress the native hover/active border — container handles it */
|
|
308
|
+
box-shadow: none;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.a1-field--has-prefix .a1-field__input:hover,
|
|
312
|
+
.a1-field--has-unit .a1-field__input:hover,
|
|
313
|
+
.a1-field--has-prefix .a1-field__input:active,
|
|
314
|
+
.a1-field--has-unit .a1-field__input:active {
|
|
315
|
+
background: transparent;
|
|
316
|
+
border-color: transparent;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Focus ring moves to container */
|
|
320
|
+
.a1-field--has-prefix .a1-field__control:focus-within,
|
|
321
|
+
.a1-field--has-unit .a1-field__control:focus-within {
|
|
322
|
+
outline: var(--a1-field-focus-ring-width) solid var(--a1-field-focus-ring-color);
|
|
323
|
+
outline-offset: var(--a1-field-focus-ring-offset);
|
|
324
|
+
border-color: var(--semantic-color-action-background);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Hover on container */
|
|
328
|
+
.a1-field--has-prefix .a1-field__control:hover:not(:focus-within),
|
|
329
|
+
.a1-field--has-unit .a1-field__control:hover:not(:focus-within) {
|
|
330
|
+
background: var(--a1-field-hover-background);
|
|
331
|
+
border-color: var(--a1-field-hover-border-color);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Disabled: suppress hover on container */
|
|
335
|
+
.a1-field--disabled.a1-field--has-prefix .a1-field__control,
|
|
336
|
+
.a1-field--disabled.a1-field--has-unit .a1-field__control {
|
|
337
|
+
background: var(--semantic-color-surface-raised);
|
|
338
|
+
border-color: var(--semantic-color-border-subtle);
|
|
339
|
+
cursor: not-allowed;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* Error: container */
|
|
343
|
+
.a1-field--error.a1-field--has-prefix .a1-field__control,
|
|
344
|
+
.a1-field--error.a1-field--has-unit .a1-field__control {
|
|
345
|
+
border-color: var(--semantic-color-status-error-border);
|
|
346
|
+
border-left-width: var(--a1-field-accent-border-width);
|
|
347
|
+
border-left-color: var(--semantic-color-status-error-background);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.a1-field--error.a1-field--has-prefix .a1-field__control:focus-within,
|
|
351
|
+
.a1-field--error.a1-field--has-unit .a1-field__control:focus-within {
|
|
352
|
+
border-color: var(--semantic-color-status-error-background);
|
|
353
|
+
outline-color: var(--semantic-color-status-error-background);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* Required: container */
|
|
357
|
+
.a1-field--required.a1-field--has-prefix .a1-field__control,
|
|
358
|
+
.a1-field--required.a1-field--has-unit .a1-field__control {
|
|
359
|
+
border-color: var(--semantic-color-status-info-border);
|
|
360
|
+
border-left-width: var(--a1-field-accent-border-width);
|
|
361
|
+
border-left-color: var(--semantic-color-status-info-background);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ─── Prefix: same size/color as value, visually before the input ────────── */
|
|
365
|
+
|
|
366
|
+
.a1-field__prefix {
|
|
367
|
+
order: -1; /* renders after input in DOM; flex order places it first */
|
|
368
|
+
flex-shrink: 0;
|
|
369
|
+
padding-inline-start: var(--a1-field-padding-inline);
|
|
370
|
+
padding-inline-end: var(--base-spacing-2);
|
|
371
|
+
font-family: var(--component-paragraph-font-family);
|
|
372
|
+
font-size: var(--a1-field-font-size);
|
|
373
|
+
font-weight: var(--base-font-weight-regular);
|
|
374
|
+
color: var(--semantic-color-text-default);
|
|
375
|
+
line-height: 1;
|
|
376
|
+
pointer-events: none;
|
|
377
|
+
user-select: none;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.a1-field--has-prefix .a1-field__input {
|
|
381
|
+
flex: 1;
|
|
382
|
+
min-width: 0;
|
|
383
|
+
padding-inline-start: 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* ─── Unit: smaller/muted, immediately after the value ───────────────────── */
|
|
387
|
+
/* Unit fills remaining space so clicks anywhere right of the value focus the input. */
|
|
388
|
+
|
|
389
|
+
.a1-field__unit {
|
|
390
|
+
flex: 1;
|
|
391
|
+
padding-inline-start: var(--base-spacing-4);
|
|
392
|
+
padding-inline-end: var(--a1-field-padding-inline);
|
|
393
|
+
font-family: var(--component-paragraph-font-family);
|
|
394
|
+
font-size: var(--a1-field-font-size);
|
|
395
|
+
color: var(--semantic-color-text-muted);
|
|
396
|
+
line-height: 1;
|
|
397
|
+
pointer-events: auto;
|
|
398
|
+
user-select: none;
|
|
399
|
+
cursor: text;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* Input shrinks to its value so the unit sits right beside it.
|
|
403
|
+
JS sets --a1-field-number-width per keystroke. field-sizing: content
|
|
404
|
+
auto-sizes in Chrome 123+ / Firefox 128+ regardless. */
|
|
405
|
+
.a1-field--has-unit .a1-field__input {
|
|
406
|
+
flex: 0 0 auto;
|
|
407
|
+
width: calc(var(--a1-field-number-width, 4ch) + var(--a1-field-padding-inline));
|
|
408
|
+
min-width: 2ch;
|
|
409
|
+
padding-inline-end: 0;
|
|
410
|
+
field-sizing: content;
|
|
411
|
+
}
|
|
412
|
+
|
|
282
413
|
/* ─── Mask overlay (PhoneField, ZipField) ─────────────────────────────────── */
|
|
283
414
|
|
|
284
415
|
.a1-field__mask-overlay {
|
|
@@ -7,6 +7,8 @@ export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonEl
|
|
|
7
7
|
label: string;
|
|
8
8
|
/** Visual style. Default: "tertiary" */
|
|
9
9
|
variant?: "tertiary" | "secondary" | "destructive" | "success";
|
|
10
|
+
/** Button size. "lg" matches Button's large touch target (3.5rem) and icon size, suitable for pairing with large Buttons. Default: "md" */
|
|
11
|
+
size?: "md" | "lg";
|
|
10
12
|
disabled?: boolean;
|
|
11
13
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
12
14
|
}
|
|
@@ -2,20 +2,24 @@ import "./icon-button.css";
|
|
|
2
2
|
import { Icon } from "../icon/Icon.jsx";
|
|
3
3
|
|
|
4
4
|
const variants = ["tertiary", "secondary", "destructive", "success"];
|
|
5
|
+
const sizes = ["md", "lg"];
|
|
5
6
|
|
|
6
7
|
export function IconButton({
|
|
7
8
|
icon,
|
|
8
9
|
label,
|
|
9
10
|
variant = "tertiary",
|
|
11
|
+
size,
|
|
10
12
|
disabled = false,
|
|
11
13
|
onClick,
|
|
12
14
|
className = "",
|
|
13
15
|
...props
|
|
14
16
|
}) {
|
|
15
17
|
const resolvedVariant = variants.includes(variant) ? variant : "tertiary";
|
|
18
|
+
const resolvedSize = sizes.includes(size) ? size : null;
|
|
16
19
|
const classes = [
|
|
17
20
|
"a1-icon-button",
|
|
18
21
|
`a1-icon-button--${resolvedVariant}`,
|
|
22
|
+
resolvedSize === "lg" && "a1-icon-button--large",
|
|
19
23
|
className,
|
|
20
24
|
].filter(Boolean).join(" ");
|
|
21
25
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
align-items: center;
|
|
9
9
|
justify-content: center;
|
|
10
10
|
flex-shrink: 0;
|
|
11
|
-
height: var(--component-icon-button-size);
|
|
11
|
+
height: var(--a1-icon-button-size, var(--component-icon-button-size));
|
|
12
12
|
aspect-ratio: 1;
|
|
13
13
|
padding: 0;
|
|
14
14
|
border-radius: var(--component-icon-button-border-radius);
|
|
@@ -33,8 +33,17 @@
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
.a1-icon-button .a1-icon {
|
|
36
|
-
font-size: var(--component-icon-button-icon-size);
|
|
37
|
-
--a1-icon-opsz: var(--component-icon-button-icon-optical-size);
|
|
36
|
+
font-size: var(--a1-icon-button-icon-size, var(--component-icon-button-icon-size));
|
|
37
|
+
--a1-icon-opsz: var(--a1-icon-button-icon-opsz, var(--component-icon-button-icon-optical-size));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ── Size: large ─────────────────────────────────────────────────────────── */
|
|
41
|
+
/* Touch target matches Button lg (3.5rem); icon matches Button lg's icon. */
|
|
42
|
+
|
|
43
|
+
.a1-icon-button--large {
|
|
44
|
+
--a1-icon-button-size: var(--component-button-large-height);
|
|
45
|
+
--a1-icon-button-icon-size: var(--component-button-icon-size);
|
|
46
|
+
--a1-icon-button-icon-opsz: var(--component-button-icon-optical-size);
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
.a1-icon-button:focus-visible {
|
|
@@ -14,7 +14,7 @@ export interface SectionProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
14
14
|
/** Background surface treatment */
|
|
15
15
|
surface?: "page" | "panel" | "raised";
|
|
16
16
|
/** Gap between direct children */
|
|
17
|
-
gap?: "xs" | "sm" | "md" | "lg";
|
|
17
|
+
gap?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
18
18
|
/** Gradient overlay colour */
|
|
19
19
|
gradient?: "accent" | "highlight" | "info" | "success" | "warn";
|
|
20
20
|
/** Gradient origin. Default: "center" */
|
|
@@ -4,7 +4,7 @@ import "./section.css";
|
|
|
4
4
|
|
|
5
5
|
const VALID_PADDING = ["lg", "md", "sm", "xs", "none"];
|
|
6
6
|
const VALID_SURFACES = ["page", "panel", "raised"];
|
|
7
|
-
const VALID_GAPS = ["xs", "sm", "md", "lg"];
|
|
7
|
+
const VALID_GAPS = ["xs", "sm", "md", "lg", "xl"];
|
|
8
8
|
const VALID_GRADIENTS = ["accent", "highlight", "info", "success", "warn"];
|
|
9
9
|
const VALID_GRADIENT_POSITIONS = [
|
|
10
10
|
"top",
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
.a1-section--gap-xs,
|
|
69
69
|
.a1-section--gap-sm,
|
|
70
70
|
.a1-section--gap-md,
|
|
71
|
-
.a1-section--gap-lg
|
|
71
|
+
.a1-section--gap-lg,
|
|
72
|
+
.a1-section--gap-xl {
|
|
72
73
|
display: grid;
|
|
73
74
|
justify-items: var(--a1-section-justify-items);
|
|
74
75
|
}
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
.a1-section--gap-sm { gap: var(--semantic-spacing-gap-sm); }
|
|
78
79
|
.a1-section--gap-md { gap: var(--semantic-spacing-gap-md); }
|
|
79
80
|
.a1-section--gap-lg { gap: var(--semantic-spacing-gap-lg); }
|
|
81
|
+
.a1-section--gap-xl { gap: var(--semantic-spacing-gap-xl); }
|
|
80
82
|
|
|
81
83
|
/* ── Height ────────────────────────────────────────────────────────────────── */
|
|
82
84
|
|
|
@@ -4,7 +4,7 @@ type Breakpoints = "xs" | "sm" | "md" | "lg" | "xl";
|
|
|
4
4
|
type Direction = "column" | "column-reverse" | "row" | "row-reverse";
|
|
5
5
|
type Justify = "start" | "center" | "end" | "between" | "around" | "evenly";
|
|
6
6
|
type Align = "stretch" | "start" | "center" | "end" | "baseline";
|
|
7
|
-
type SemanticGap = "xs" | "sm" | "md" | "lg";
|
|
7
|
+
type SemanticGap = "xs" | "sm" | "md" | "lg" | "xl";
|
|
8
8
|
type SpacingToken = 1 | 2 | 4 | 6 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 64 | 96 | 128;
|
|
9
9
|
|
|
10
10
|
export interface StackProps extends React.HTMLAttributes<HTMLElement> {
|
|
@@ -4,7 +4,7 @@ import { resolveSpacing } from "../structure-utils.js";
|
|
|
4
4
|
const directions = ["column", "column-reverse", "row", "row-reverse"];
|
|
5
5
|
const alignments = ["stretch", "start", "center", "end", "baseline"];
|
|
6
6
|
const justifications = ["start", "center", "end", "between", "around", "evenly"];
|
|
7
|
-
const semanticGaps = ["xs", "sm", "md", "lg"];
|
|
7
|
+
const semanticGaps = ["xs", "sm", "md", "lg", "xl"];
|
|
8
8
|
const breakpoints = ["xs", "sm", "md", "lg", "xl"];
|
|
9
9
|
|
|
10
10
|
function resolveGap(gap) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export interface StepTrackerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Total number of steps. */
|
|
5
|
+
steps: number;
|
|
6
|
+
/** 1-indexed position of the current step. Default: 1. */
|
|
7
|
+
currentStep?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Horizontal alignment of the tracker within its container.
|
|
10
|
+
* "left" | "center" | "right" — groups items together.
|
|
11
|
+
* "full" — active pill expands to fill remaining space.
|
|
12
|
+
* Default: "left".
|
|
13
|
+
*/
|
|
14
|
+
align?: "left" | "center" | "right" | "full";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export declare function StepTracker(props: StepTrackerProps): React.ReactElement;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import "./step-tracker.css";
|
|
2
|
+
|
|
3
|
+
const ALIGNS = ["left", "center", "right", "full"];
|
|
4
|
+
|
|
5
|
+
export function StepTracker({
|
|
6
|
+
steps,
|
|
7
|
+
currentStep = 1,
|
|
8
|
+
align = "left",
|
|
9
|
+
className = "",
|
|
10
|
+
...props
|
|
11
|
+
}) {
|
|
12
|
+
const resolvedAlign = ALIGNS.includes(align) ? align : "left";
|
|
13
|
+
const total = Math.max(1, steps);
|
|
14
|
+
const current = Math.min(total, Math.max(1, currentStep));
|
|
15
|
+
|
|
16
|
+
const classes = [
|
|
17
|
+
"a1-step-tracker",
|
|
18
|
+
resolvedAlign !== "left" && `a1-step-tracker--${resolvedAlign}`,
|
|
19
|
+
className,
|
|
20
|
+
].filter(Boolean).join(" ");
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={classes}
|
|
25
|
+
role="img"
|
|
26
|
+
aria-label={`Step ${current} of ${total}`}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{Array.from({ length: total }, (_, i) => (
|
|
30
|
+
<div
|
|
31
|
+
key={i}
|
|
32
|
+
className={[
|
|
33
|
+
"a1-step-tracker__step",
|
|
34
|
+
i + 1 === current && "a1-step-tracker__step--current",
|
|
35
|
+
].filter(Boolean).join(" ")}
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
/>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.a1-step-tracker {
|
|
2
|
+
--a1-st-dot-size: var(--component-step-tracker-dot-size);
|
|
3
|
+
--a1-st-active-width: var(--component-step-tracker-active-width);
|
|
4
|
+
--a1-st-dot-color: var(--semantic-color-border-strong);
|
|
5
|
+
--a1-st-active-color: var(--semantic-color-text-default);
|
|
6
|
+
--a1-st-gap: var(--component-step-tracker-gap);
|
|
7
|
+
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: var(--a1-st-gap);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ─── Alignment ───────────────────────────────────────────────────────────── */
|
|
14
|
+
|
|
15
|
+
.a1-step-tracker--center { justify-content: center; }
|
|
16
|
+
.a1-step-tracker--right { justify-content: flex-end; }
|
|
17
|
+
|
|
18
|
+
/* full: dots stay fixed, active pill expands to fill remaining space */
|
|
19
|
+
.a1-step-tracker--full .a1-step-tracker__step--current {
|
|
20
|
+
flex: 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ─── Step ────────────────────────────────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
.a1-step-tracker__step {
|
|
26
|
+
block-size: var(--a1-st-dot-size);
|
|
27
|
+
inline-size: var(--a1-st-dot-size);
|
|
28
|
+
border-radius: 9999px;
|
|
29
|
+
background: var(--a1-st-dot-color);
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
transition: inline-size var(--semantic-motion-duration-fast, 150ms) var(--semantic-motion-easing-standard, ease);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.a1-step-tracker__step--current {
|
|
35
|
+
inline-size: var(--a1-st-active-width);
|
|
36
|
+
background: var(--a1-st-active-color);
|
|
37
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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={[
|
|
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/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { Notification } from "./components/notification/Notification.jsx";
|
|
|
7
7
|
export { Snackbar } from "./components/snackbar/Snackbar.jsx";
|
|
8
8
|
export { StatusBar } from "./components/status-bar/StatusBar.jsx";
|
|
9
9
|
export { CircularProgress } from "./components/circular-progress/CircularProgress.jsx";
|
|
10
|
+
export { StepTracker } from "./components/step-tracker/StepTracker.jsx";
|
|
10
11
|
export { Bleed } from "./components/bleed/Bleed.jsx";
|
|
11
12
|
export { IconButton } from "./components/icon-button/IconButton.jsx";
|
|
12
13
|
export { Button } from "./components/button/Button.jsx";export { ButtonContainer } from "./components/button-container/ButtonContainer.jsx";
|
package/src/themes.css
CHANGED
|
@@ -295,10 +295,10 @@ html.a1-theme-catlympics.a1-theme-light, html.a1-theme-light.a1-theme-catlympics
|
|
|
295
295
|
|
|
296
296
|
/* ────────────────────────────────────────────────────────────
|
|
297
297
|
Fresh
|
|
298
|
-
A crisp, airy theme with sky-blue accents, Nunito body text (ExtraBold headings), Baskerville display typography, and a cool mint gradient background. Apply
|
|
298
|
+
A crisp, airy theme with sky-blue accents, Nunito body text (ExtraBold headings), Baskerville display typography, and a cool mint gradient background. Apply class="a1-theme-fresh" to <html>.
|
|
299
299
|
──────────────────────────────────────────────────────────── */
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
html.a1-theme-fresh {
|
|
302
302
|
--base-color-accent-0: #FAFCFF;
|
|
303
303
|
--base-color-accent-50: #ECF3FE;
|
|
304
304
|
--base-color-accent-100: #D8E8FD;
|
|
@@ -432,8 +432,6 @@ html.a1-theme-catlympics.a1-theme-light, html.a1-theme-light.a1-theme-catlympics
|
|
|
432
432
|
--component-paragraph-font-weight: var(--theme-a1-fresh-font-weight-body);
|
|
433
433
|
--component-heading-font-weight-heading: var(--theme-a1-fresh-font-weight-heading);
|
|
434
434
|
--component-heading-font-weight-display: var(--theme-a1-fresh-font-weight-display);
|
|
435
|
-
background-image: linear-gradient(207.43deg, #D7FFF8 0%, #CDF5EE 57.44%);
|
|
436
|
-
background-attachment: fixed;
|
|
437
435
|
}
|
|
438
436
|
|
|
439
437
|
|
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;
|
|
@@ -234,6 +235,7 @@
|
|
|
234
235
|
--semantic-spacing-gap-sm: 0.75rem;
|
|
235
236
|
--semantic-spacing-gap-md: 1rem;
|
|
236
237
|
--semantic-spacing-gap-lg: 1.5rem;
|
|
238
|
+
--semantic-spacing-gap-xl: 2.5rem;
|
|
237
239
|
--semantic-font-size-body-xs: 0.75rem;
|
|
238
240
|
--semantic-font-size-body-sm: 0.875rem;
|
|
239
241
|
--semantic-font-size-body-md: 1rem;
|
|
@@ -413,6 +415,7 @@
|
|
|
413
415
|
--component-circular-progress-sm-size: 4rem;
|
|
414
416
|
--component-circular-progress-md-size: 6rem;
|
|
415
417
|
--component-circular-progress-lg-size: 8rem;
|
|
418
|
+
--component-circular-progress-xl-size: 12rem;
|
|
416
419
|
--component-circular-progress-track-color: #e1e8f3;
|
|
417
420
|
--component-circular-progress-fill-color: #7c3aed;
|
|
418
421
|
--component-circular-progress-gap: 0.5rem;
|
|
@@ -679,6 +682,11 @@
|
|
|
679
682
|
--component-status-bar-fill-background: #7c3aed;
|
|
680
683
|
--component-status-bar-label-gap: 0.5rem;
|
|
681
684
|
--component-status-bar-indeterminate-duration: 1400ms;
|
|
685
|
+
--component-step-tracker-dot-size: 0.5rem;
|
|
686
|
+
--component-step-tracker-dot-color: #64748b;
|
|
687
|
+
--component-step-tracker-active-width: 2rem;
|
|
688
|
+
--component-step-tracker-active-color: #060b14;
|
|
689
|
+
--component-step-tracker-gap: 0.375rem;
|
|
682
690
|
--component-switch-track-width: 2.5rem;
|
|
683
691
|
--component-switch-track-height: 1.375rem;
|
|
684
692
|
--component-switch-thumb-size: 1rem;
|
|
@@ -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
|
-
}
|