@gtivr4/a1-design-system-react 0.7.0 → 0.9.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 +5 -0
- package/src/components/bottom-drawer/BottomDrawer.d.ts +34 -0
- package/src/components/bottom-drawer/BottomDrawer.jsx +55 -0
- package/src/components/bottom-drawer/bottom-drawer.css +138 -0
- package/src/components/card/card.css +1 -1
- 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/field/field.css +1 -1
- package/src/components/top-header/TopHeader.jsx +81 -12
- package/src/components/top-header/top-header.css +57 -0
- package/src/index.js +1 -0
- package/src/themes.css +1 -0
- package/src/tokens.css +7 -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
|
@@ -43,6 +43,7 @@ body {
|
|
|
43
43
|
.a1-inverse {
|
|
44
44
|
color-scheme: dark;
|
|
45
45
|
--semantic-color-surface-page: var(--base-color-neutral-900);
|
|
46
|
+
--semantic-color-surface-card: var(--base-color-neutral-800);
|
|
46
47
|
--semantic-color-surface-panel: var(--base-color-neutral-800);
|
|
47
48
|
--semantic-color-surface-raised: var(--base-color-neutral-700);
|
|
48
49
|
--semantic-color-text-default: var(--base-color-neutral-50);
|
|
@@ -122,6 +123,7 @@ body {
|
|
|
122
123
|
:root {
|
|
123
124
|
color-scheme: dark;
|
|
124
125
|
--semantic-color-surface-page: var(--base-color-neutral-900);
|
|
126
|
+
--semantic-color-surface-card: var(--base-color-neutral-800);
|
|
125
127
|
--semantic-color-surface-panel: var(--base-color-neutral-800);
|
|
126
128
|
--semantic-color-surface-raised: var(--base-color-neutral-700);
|
|
127
129
|
--semantic-color-text-default: var(--base-color-neutral-50);
|
|
@@ -196,6 +198,7 @@ body {
|
|
|
196
198
|
.a1-inverse {
|
|
197
199
|
color-scheme: light;
|
|
198
200
|
--semantic-color-surface-page: var(--base-color-neutral-0);
|
|
201
|
+
--semantic-color-surface-card: var(--base-color-neutral-0);
|
|
199
202
|
--semantic-color-surface-panel: var(--base-color-neutral-50);
|
|
200
203
|
--semantic-color-surface-raised: var(--base-color-neutral-100);
|
|
201
204
|
--semantic-color-text-default: var(--base-color-neutral-900);
|
|
@@ -273,6 +276,7 @@ body {
|
|
|
273
276
|
html.a1-theme-dark {
|
|
274
277
|
color-scheme: dark;
|
|
275
278
|
--semantic-color-surface-page: var(--base-color-neutral-900);
|
|
279
|
+
--semantic-color-surface-card: var(--base-color-neutral-800);
|
|
276
280
|
--semantic-color-surface-panel: var(--base-color-neutral-800);
|
|
277
281
|
--semantic-color-surface-raised: var(--base-color-neutral-700);
|
|
278
282
|
--semantic-color-text-default: var(--base-color-neutral-50);
|
|
@@ -348,6 +352,7 @@ html.a1-theme-dark .a1-inverse,
|
|
|
348
352
|
html.a1-theme-dark .a1-theme-light {
|
|
349
353
|
color-scheme: light;
|
|
350
354
|
--semantic-color-surface-page: var(--base-color-neutral-0);
|
|
355
|
+
--semantic-color-surface-card: var(--base-color-neutral-0);
|
|
351
356
|
--semantic-color-surface-panel: var(--base-color-neutral-50);
|
|
352
357
|
--semantic-color-surface-raised: var(--base-color-neutral-100);
|
|
353
358
|
--semantic-color-text-default: var(--base-color-neutral-900);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface BottomDrawerItem {
|
|
4
|
+
/** Unique identifier for the item. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Display label shown below the icon. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Material Symbols icon name. */
|
|
9
|
+
icon: string;
|
|
10
|
+
/** Navigate to this URL — renders an <a> element. */
|
|
11
|
+
href?: string;
|
|
12
|
+
/** Click handler — used when no href is provided; renders a <button>. */
|
|
13
|
+
onClick?: () => void;
|
|
14
|
+
/** Marks this item as the currently active destination. */
|
|
15
|
+
active?: boolean;
|
|
16
|
+
/** Numeric badge count shown on the icon (capped at 99+). */
|
|
17
|
+
badge?: number;
|
|
18
|
+
/** Disables the item. */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BottomDrawerProps {
|
|
23
|
+
/**
|
|
24
|
+
* Up to 5 navigation items. Additional items beyond 5 are silently ignored.
|
|
25
|
+
*/
|
|
26
|
+
items: BottomDrawerItem[];
|
|
27
|
+
/**
|
|
28
|
+
* Accessible name for the nav element.
|
|
29
|
+
* @default "Primary navigation"
|
|
30
|
+
*/
|
|
31
|
+
"aria-label"?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare function BottomDrawer(props: BottomDrawerProps): ReactNode;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Icon } from "../icon/Icon.jsx";
|
|
2
|
+
import "./bottom-drawer.css";
|
|
3
|
+
|
|
4
|
+
export function BottomDrawer({ items = [], "aria-label": ariaLabel = "Primary navigation", className = "" }) {
|
|
5
|
+
const visibleItems = items.slice(0, 5);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<nav className={["a1-bottom-drawer", className].filter(Boolean).join(" ")} aria-label={ariaLabel}>
|
|
9
|
+
<ul className="a1-bottom-drawer__list" role="list">
|
|
10
|
+
{visibleItems.map((item) => {
|
|
11
|
+
const Tag = item.href ? "a" : "button";
|
|
12
|
+
const tagProps = item.href
|
|
13
|
+
? { href: item.href }
|
|
14
|
+
: { type: "button", onClick: item.onClick };
|
|
15
|
+
|
|
16
|
+
const linkClass = [
|
|
17
|
+
"a1-bottom-drawer__link",
|
|
18
|
+
item.active && "a1-bottom-drawer__link--active",
|
|
19
|
+
item.disabled && "a1-bottom-drawer__link--disabled",
|
|
20
|
+
]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(" ");
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<li key={item.id} className="a1-bottom-drawer__item">
|
|
26
|
+
<Tag
|
|
27
|
+
className={linkClass}
|
|
28
|
+
aria-current={item.active ? "page" : undefined}
|
|
29
|
+
aria-disabled={item.disabled ? "true" : undefined}
|
|
30
|
+
{...tagProps}
|
|
31
|
+
>
|
|
32
|
+
<span className="a1-bottom-drawer__link-icon-wrap">
|
|
33
|
+
<Icon
|
|
34
|
+
name={item.icon}
|
|
35
|
+
className="a1-bottom-drawer__link-icon"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
/>
|
|
38
|
+
{item.badge > 0 && (
|
|
39
|
+
<span
|
|
40
|
+
className="a1-bottom-drawer__badge"
|
|
41
|
+
aria-label={`${item.badge} unread`}
|
|
42
|
+
>
|
|
43
|
+
{item.badge > 99 ? "99+" : item.badge}
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="a1-bottom-drawer__link-label">{item.label}</span>
|
|
48
|
+
</Tag>
|
|
49
|
+
</li>
|
|
50
|
+
);
|
|
51
|
+
})}
|
|
52
|
+
</ul>
|
|
53
|
+
</nav>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
BottomDrawer
|
|
3
|
+
Fixed bottom navigation bar — up to 5 items, icon stacked above label.
|
|
4
|
+
Mirrors the TopHeader icon-above item style via shared --a1-nav-stacked-*
|
|
5
|
+
custom properties.
|
|
6
|
+
═══════════════════════════════════════════════════════════════════════════ */
|
|
7
|
+
|
|
8
|
+
/* ── Shared stacked-nav-item variables ───────────────────────────────────── */
|
|
9
|
+
/* These same names are referenced in top-header.css icon-above mode so both
|
|
10
|
+
components stay in sync when tokens change. */
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--a1-nav-stacked-icon-size: var(--semantic-font-size-heading-sm);
|
|
14
|
+
--a1-nav-stacked-label-size: var(--semantic-font-size-body-xs);
|
|
15
|
+
--a1-nav-stacked-gap: var(--base-spacing-2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* ── Shell ────────────────────────────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
.a1-bottom-drawer {
|
|
21
|
+
position: fixed;
|
|
22
|
+
inset-block-end: 0;
|
|
23
|
+
inset-inline: 0;
|
|
24
|
+
z-index: var(--component-bottom-drawer-z-index);
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: stretch;
|
|
27
|
+
min-block-size: var(--component-bottom-drawer-height);
|
|
28
|
+
padding-block-end: env(safe-area-inset-bottom, 0px);
|
|
29
|
+
background: var(--semantic-color-surface-page);
|
|
30
|
+
border-block-start: var(--component-bottom-drawer-border-width) solid var(--semantic-color-border-subtle);
|
|
31
|
+
box-shadow: 0 -4px 12px color-mix(in srgb, var(--semantic-color-text-default) 8%, transparent);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ── Item list ────────────────────────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
.a1-bottom-drawer__list {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: stretch;
|
|
39
|
+
width: 100%;
|
|
40
|
+
list-style: none;
|
|
41
|
+
margin: 0;
|
|
42
|
+
padding: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.a1-bottom-drawer__item {
|
|
46
|
+
flex: 1;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: stretch;
|
|
49
|
+
min-inline-size: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ── Nav link — icon above label ─────────────────────────────────────────── */
|
|
53
|
+
|
|
54
|
+
.a1-bottom-drawer__link {
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
gap: var(--a1-nav-stacked-gap);
|
|
60
|
+
width: 100%;
|
|
61
|
+
padding-block: var(--base-spacing-8);
|
|
62
|
+
padding-inline: var(--base-spacing-4);
|
|
63
|
+
color: var(--semantic-color-text-muted);
|
|
64
|
+
font-family: var(--component-paragraph-font-family);
|
|
65
|
+
font-size: var(--a1-nav-stacked-label-size);
|
|
66
|
+
font-weight: 500;
|
|
67
|
+
line-height: 1;
|
|
68
|
+
text-decoration: none;
|
|
69
|
+
background: transparent;
|
|
70
|
+
border: none;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
transition:
|
|
73
|
+
color var(--semantic-motion-duration-fast) var(--semantic-motion-easing-standard),
|
|
74
|
+
background var(--semantic-motion-duration-fast) var(--semantic-motion-easing-standard);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.a1-bottom-drawer__link:hover:not(.a1-bottom-drawer__link--disabled) {
|
|
78
|
+
color: var(--semantic-color-text-default);
|
|
79
|
+
background: var(--semantic-color-surface-raised);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.a1-bottom-drawer__link:focus-visible {
|
|
83
|
+
outline: 2px solid var(--semantic-color-action-background);
|
|
84
|
+
outline-offset: -2px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.a1-bottom-drawer__link--active {
|
|
88
|
+
color: var(--semantic-color-action-background);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.a1-bottom-drawer__link--disabled {
|
|
92
|
+
opacity: var(--component-button-disabled-opacity);
|
|
93
|
+
cursor: not-allowed;
|
|
94
|
+
pointer-events: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── Icon ─────────────────────────────────────────────────────────────────── */
|
|
98
|
+
|
|
99
|
+
.a1-bottom-drawer__link-icon-wrap {
|
|
100
|
+
position: relative;
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
flex-shrink: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.a1-bottom-drawer__link-icon {
|
|
106
|
+
font-size: var(--a1-nav-stacked-icon-size);
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ── Badge ────────────────────────────────────────────────────────────────── */
|
|
111
|
+
|
|
112
|
+
.a1-bottom-drawer__badge {
|
|
113
|
+
position: absolute;
|
|
114
|
+
top: -3px;
|
|
115
|
+
right: -6px;
|
|
116
|
+
min-inline-size: 16px;
|
|
117
|
+
block-size: 16px;
|
|
118
|
+
padding-inline: var(--base-spacing-4);
|
|
119
|
+
background: var(--semantic-color-status-error-background);
|
|
120
|
+
color: var(--semantic-color-status-error-foreground);
|
|
121
|
+
border-radius: 8px;
|
|
122
|
+
font-family: var(--component-paragraph-font-family);
|
|
123
|
+
font-size: var(--component-tab-count-font-size, 10px);
|
|
124
|
+
font-weight: 700;
|
|
125
|
+
line-height: 16px;
|
|
126
|
+
text-align: center;
|
|
127
|
+
white-space: nowrap;
|
|
128
|
+
pointer-events: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── Label ────────────────────────────────────────────────────────────────── */
|
|
132
|
+
|
|
133
|
+
.a1-bottom-drawer__link-label {
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
text-overflow: ellipsis;
|
|
136
|
+
white-space: nowrap;
|
|
137
|
+
max-inline-size: 100%;
|
|
138
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
.a1-card {
|
|
2
2
|
container: a1-card / inline-size;
|
|
3
3
|
box-sizing: border-box;
|
|
4
|
-
background: var(--semantic-color-surface-
|
|
4
|
+
background: var(--semantic-color-surface-card);
|
|
5
5
|
border: var(--component-card-border-width) solid var(--semantic-color-border-subtle);
|
|
6
6
|
border-radius: var(--component-card-border-radius);
|
|
7
7
|
padding: var(--component-card-padding);
|
|
@@ -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
|
|
|
@@ -391,7 +391,7 @@
|
|
|
391
391
|
padding-inline-start: var(--base-spacing-4);
|
|
392
392
|
padding-inline-end: var(--a1-field-padding-inline);
|
|
393
393
|
font-family: var(--component-paragraph-font-family);
|
|
394
|
-
font-size: var(--
|
|
394
|
+
font-size: var(--a1-field-font-size);
|
|
395
395
|
color: var(--semantic-color-text-muted);
|
|
396
396
|
line-height: 1;
|
|
397
397
|
pointer-events: auto;
|
|
@@ -8,6 +8,28 @@ import "./top-header.css";
|
|
|
8
8
|
|
|
9
9
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
10
10
|
|
|
11
|
+
// Breakpoint min-widths that match the system breakpoint tokens.
|
|
12
|
+
const BP_QUERIES = {
|
|
13
|
+
sm: "(min-width: 481px)",
|
|
14
|
+
md: "(min-width: 641px)",
|
|
15
|
+
lg: "(min-width: 1025px)",
|
|
16
|
+
xl: "(min-width: 1441px)",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Resolve a scalar or responsive { xs?, sm?, md?, lg?, xl? } navIconPosition
|
|
20
|
+
// value to the mode active at the current viewport: "start" | "above" | "hidden".
|
|
21
|
+
function resolveNavMode(prop) {
|
|
22
|
+
if (!prop || typeof prop === "string") return prop ?? "start";
|
|
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; // "start" | "above" | "hidden"
|
|
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 [navMode, setNavMode] = useState(() => resolveNavMode(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,30 @@ export function TopHeader({
|
|
|
569
617
|
return () => desktopQuery.removeListener(closeAtDesktop);
|
|
570
618
|
}, [mobileNavOpen]);
|
|
571
619
|
|
|
620
|
+
// Re-resolve navMode whenever navIconPosition or the viewport changes.
|
|
621
|
+
useEffect(() => {
|
|
622
|
+
if (typeof window === "undefined" || !window.matchMedia) return undefined;
|
|
623
|
+
|
|
624
|
+
const update = () => setNavMode(resolveNavMode(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
|
+
navMode === "above" && "a1-top-header--nav-icon-above",
|
|
641
|
+
navMode === "hidden" && "a1-top-header--nav-hidden",
|
|
642
|
+
className,
|
|
643
|
+
].filter(Boolean).join(" ")}
|
|
576
644
|
>
|
|
577
645
|
<button
|
|
578
646
|
type="button"
|
|
@@ -599,6 +667,7 @@ export function TopHeader({
|
|
|
599
667
|
item={item}
|
|
600
668
|
openId={openSubmenu}
|
|
601
669
|
onOpen={setOpenSubmenu}
|
|
670
|
+
iconAbove={navMode === "above"}
|
|
602
671
|
/>
|
|
603
672
|
))}
|
|
604
673
|
</ul>
|
|
@@ -630,7 +699,7 @@ export function TopHeader({
|
|
|
630
699
|
</div>
|
|
631
700
|
</header>
|
|
632
701
|
|
|
633
|
-
{mobileNavOpen && (
|
|
702
|
+
{mobileNavOpen && navMode !== "hidden" && (
|
|
634
703
|
<MobileDrawer
|
|
635
704
|
navItems={navItems}
|
|
636
705
|
onClose={() => setMobileNavOpen(false)}
|
|
@@ -320,6 +320,44 @@
|
|
|
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
|
+
Uses the same --a1-nav-stacked-* variables as BottomDrawer so both stay
|
|
327
|
+
visually in sync. */
|
|
328
|
+
.a1-top-header--nav-icon-above .a1-top-header__nav-link {
|
|
329
|
+
flex-direction: column;
|
|
330
|
+
align-items: center;
|
|
331
|
+
gap: var(--a1-nav-stacked-gap, var(--base-spacing-2));
|
|
332
|
+
padding-block: var(--base-spacing-8);
|
|
333
|
+
padding-inline: var(--base-spacing-8);
|
|
334
|
+
font-size: var(--a1-nav-stacked-label-size, var(--semantic-font-size-body-xs));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Icon slightly larger so it reads clearly at the top of each item. */
|
|
338
|
+
.a1-top-header--nav-icon-above .a1-top-header__nav-link-icon {
|
|
339
|
+
font-size: var(--a1-nav-stacked-icon-size, var(--semantic-font-size-heading-sm));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* Nav-hidden mode: BottomDrawer provides nav — suppress hamburger and nav entirely. */
|
|
343
|
+
.a1-top-header--nav-hidden .a1-top-header__hamburger { display: none; }
|
|
344
|
+
.a1-top-header--nav-hidden .a1-top-header__nav { display: none; }
|
|
345
|
+
|
|
346
|
+
/* Label row: inline-flex so the small chevron sits beside the text. */
|
|
347
|
+
.a1-top-header--nav-icon-above .a1-top-header__nav-link-label {
|
|
348
|
+
display: inline-flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
gap: var(--base-spacing-2);
|
|
351
|
+
line-height: 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Chevron in this context is a small inline decoration next to the label. */
|
|
355
|
+
.a1-top-header--nav-icon-above .a1-top-header__nav-link-label .a1-top-header__nav-chevron {
|
|
356
|
+
font-size: var(--semantic-font-size-body-sm);
|
|
357
|
+
margin-inline-start: 0;
|
|
358
|
+
transition: transform var(--semantic-motion-duration-fast) var(--semantic-motion-easing-standard);
|
|
359
|
+
}
|
|
360
|
+
|
|
323
361
|
/* ── Responsive ──────────────────────────────────────────────────────────── */
|
|
324
362
|
|
|
325
363
|
@media (max-width: 768px) {
|
|
@@ -334,4 +372,23 @@
|
|
|
334
372
|
.a1-top-header__nav {
|
|
335
373
|
display: none;
|
|
336
374
|
}
|
|
375
|
+
|
|
376
|
+
/* Icon-above mode: nav is always visible; hamburger is not needed. */
|
|
377
|
+
.a1-top-header--nav-icon-above .a1-top-header__hamburger {
|
|
378
|
+
display: none;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.a1-top-header--nav-icon-above .a1-top-header__nav {
|
|
382
|
+
display: flex;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* Allow horizontal scrolling if items overflow on very small screens. */
|
|
386
|
+
.a1-top-header--nav-icon-above .a1-top-header__nav-list {
|
|
387
|
+
overflow-x: auto;
|
|
388
|
+
scrollbar-width: none;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.a1-top-header--nav-icon-above .a1-top-header__nav-list::-webkit-scrollbar {
|
|
392
|
+
display: none;
|
|
393
|
+
}
|
|
337
394
|
}
|
package/src/index.js
CHANGED
|
@@ -54,6 +54,7 @@ export { LabelsProvider, useLabel } from "./components/labels/Labels.jsx";
|
|
|
54
54
|
export { Menu, MenuSection, MenuItem } from "./components/menu/Menu.jsx";
|
|
55
55
|
export { SideNav, SideNavItem, SideNavGroup } from "./components/side-nav/SideNav.jsx";
|
|
56
56
|
export { TokenSelect } from "./components/token-select/TokenSelect.jsx";
|
|
57
|
+
export { BottomDrawer } from "./components/bottom-drawer/BottomDrawer.jsx";
|
|
57
58
|
export { TopHeader } from "./components/top-header/TopHeader.jsx";
|
|
58
59
|
export { DataTable } from "./components/data-table/DataTable.jsx";
|
|
59
60
|
export { DataTableFilters } from "./components/data-table/DataTableFilters.jsx";
|
package/src/themes.css
CHANGED
|
@@ -364,6 +364,7 @@ html.a1-theme-fresh {
|
|
|
364
364
|
--semantic-color-text-inverse: var(--base-color-neutral-0);
|
|
365
365
|
--semantic-color-text-accent: var(--base-color-accent-600);
|
|
366
366
|
--semantic-color-surface-page: #D7FFF8;
|
|
367
|
+
--semantic-color-surface-card: var(--base-color-neutral-0);
|
|
367
368
|
--semantic-color-surface-panel: var(--base-color-neutral-0);
|
|
368
369
|
--semantic-color-surface-raised: var(--base-color-neutral-50);
|
|
369
370
|
--semantic-color-border-subtle: var(--base-color-neutral-200);
|
package/src/tokens.css
CHANGED
|
@@ -127,9 +127,11 @@
|
|
|
127
127
|
--base-spacing-32: 2rem;
|
|
128
128
|
--base-spacing-40: 2.5rem;
|
|
129
129
|
--base-spacing-48: 3rem;
|
|
130
|
+
--base-spacing-56: 3.5rem;
|
|
130
131
|
--base-spacing-64: 4rem;
|
|
131
132
|
--base-spacing-96: 6rem;
|
|
132
133
|
--base-spacing-128: 8rem;
|
|
134
|
+
--base-spacing-192: 12rem;
|
|
133
135
|
--base-spacing-neg-4: -0.25rem;
|
|
134
136
|
--base-spacing-neg-8: -0.5rem;
|
|
135
137
|
--base-spacing-neg-12: -0.75rem;
|
|
@@ -180,6 +182,7 @@
|
|
|
180
182
|
--semantic-color-text-inverse: #ffffff;
|
|
181
183
|
--semantic-color-text-accent: #7c3aed;
|
|
182
184
|
--semantic-color-surface-page: #ffffff;
|
|
185
|
+
--semantic-color-surface-card: #ffffff;
|
|
183
186
|
--semantic-color-surface-panel: #f0f6fe;
|
|
184
187
|
--semantic-color-surface-raised: #e1e8f3;
|
|
185
188
|
--semantic-color-surface-inverse: #060b14;
|
|
@@ -332,6 +335,9 @@
|
|
|
332
335
|
--component-blockquote-padding-inline: 1.5rem;
|
|
333
336
|
--component-blockquote-mark-size: 5rem;
|
|
334
337
|
--component-blockquote-cite-font-weight: 500;
|
|
338
|
+
--component-bottom-drawer-height: 3.5rem;
|
|
339
|
+
--component-bottom-drawer-border-width: 1px;
|
|
340
|
+
--component-bottom-drawer-z-index: 200;
|
|
335
341
|
--component-calendar-month-gap: 2rem;
|
|
336
342
|
--component-calendar-heading-padding-block: 0.75rem;
|
|
337
343
|
--component-calendar-heading-padding-block-compact: 0.5rem;
|
|
@@ -414,6 +420,7 @@
|
|
|
414
420
|
--component-circular-progress-sm-size: 4rem;
|
|
415
421
|
--component-circular-progress-md-size: 6rem;
|
|
416
422
|
--component-circular-progress-lg-size: 8rem;
|
|
423
|
+
--component-circular-progress-xl-size: 12rem;
|
|
417
424
|
--component-circular-progress-track-color: #e1e8f3;
|
|
418
425
|
--component-circular-progress-fill-color: #7c3aed;
|
|
419
426
|
--component-circular-progress-gap: 0.5rem;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
export interface SystemBannerProps {
|
|
4
|
-
/** Semantic status colour. Default: "neutral" */
|
|
5
|
-
status?: "neutral" | "info" | "success" | "warn" | "error";
|
|
6
|
-
/** Bold title text */
|
|
7
|
-
title?: string;
|
|
8
|
-
/** Override the default status icon with any Material Symbols name */
|
|
9
|
-
icon?: string;
|
|
10
|
-
/** Action element rendered at the trailing end */
|
|
11
|
-
action?: React.ReactNode;
|
|
12
|
-
/** Called when the dismiss button is clicked. Omit to hide the dismiss button. */
|
|
13
|
-
onDismiss?: () => void;
|
|
14
|
-
children?: React.ReactNode;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export declare function SystemBanner(props: SystemBannerProps): React.ReactElement;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import "./system-banner.css";
|
|
2
|
-
import { Icon } from "../icon/Icon.jsx";
|
|
3
|
-
import { IconButton } from "../icon-button/IconButton.jsx";
|
|
4
|
-
|
|
5
|
-
const STATUS_ICONS = {
|
|
6
|
-
neutral: "campaign",
|
|
7
|
-
info: "info",
|
|
8
|
-
success: "check_circle",
|
|
9
|
-
warn: "warning",
|
|
10
|
-
error: "error",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const STATUSES = ["neutral", "info", "success", "warn", "error"];
|
|
14
|
-
|
|
15
|
-
export function SystemBanner({
|
|
16
|
-
status = "neutral",
|
|
17
|
-
title,
|
|
18
|
-
icon,
|
|
19
|
-
action,
|
|
20
|
-
onDismiss,
|
|
21
|
-
children,
|
|
22
|
-
}) {
|
|
23
|
-
const resolvedStatus = STATUSES.includes(status) ? status : "neutral";
|
|
24
|
-
const resolvedIcon = icon ?? STATUS_ICONS[resolvedStatus];
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div
|
|
28
|
-
className={`a1-system-banner a1-system-banner--${resolvedStatus}`}
|
|
29
|
-
role="alert"
|
|
30
|
-
aria-live="polite"
|
|
31
|
-
>
|
|
32
|
-
<div className="a1-system-banner__inner">
|
|
33
|
-
<span className="a1-system-banner__icon" aria-hidden="true">
|
|
34
|
-
<Icon name={resolvedIcon} />
|
|
35
|
-
</span>
|
|
36
|
-
|
|
37
|
-
<div className="a1-system-banner__content">
|
|
38
|
-
{title && <span className="a1-system-banner__title">{title}</span>}
|
|
39
|
-
{children && <span className="a1-system-banner__body">{children}</span>}
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
{action && (
|
|
43
|
-
<div className="a1-system-banner__action">{action}</div>
|
|
44
|
-
)}
|
|
45
|
-
|
|
46
|
-
{onDismiss && (
|
|
47
|
-
<IconButton
|
|
48
|
-
icon="close"
|
|
49
|
-
label="Dismiss"
|
|
50
|
-
onClick={onDismiss}
|
|
51
|
-
className="a1-system-banner__dismiss"
|
|
52
|
-
/>
|
|
53
|
-
)}
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/* ─── System Banner ────────────────────────────────────────────────────────── */
|
|
2
|
-
|
|
3
|
-
.a1-system-banner {
|
|
4
|
-
--a1-sysbanner-bg: var(--semantic-color-surface-inverse);
|
|
5
|
-
--a1-sysbanner-fg: var(--semantic-color-text-inverse);
|
|
6
|
-
|
|
7
|
-
background: var(--a1-sysbanner-bg);
|
|
8
|
-
color: var(--a1-sysbanner-fg);
|
|
9
|
-
width: 100%;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.a1-system-banner--info { --a1-sysbanner-bg: var(--semantic-color-status-info-background); }
|
|
13
|
-
.a1-system-banner--success { --a1-sysbanner-bg: var(--semantic-color-status-success-background); }
|
|
14
|
-
.a1-system-banner--warn { --a1-sysbanner-bg: var(--semantic-color-status-warn-background); }
|
|
15
|
-
.a1-system-banner--error { --a1-sysbanner-bg: var(--semantic-color-status-error-background); }
|
|
16
|
-
|
|
17
|
-
/* ─── Inner layout ─────────────────────────────────────────────────────────── */
|
|
18
|
-
|
|
19
|
-
.a1-system-banner__inner {
|
|
20
|
-
display: flex;
|
|
21
|
-
align-items: center;
|
|
22
|
-
flex-wrap: wrap;
|
|
23
|
-
gap: var(--base-spacing-8) var(--base-spacing-12);
|
|
24
|
-
max-width: var(--component-message-banner-system-max-width);
|
|
25
|
-
margin-inline: auto;
|
|
26
|
-
padding-block: var(--base-spacing-12);
|
|
27
|
-
padding-inline: var(--base-spacing-24);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/* ─── Icon ─────────────────────────────────────────────────────────────────── */
|
|
31
|
-
|
|
32
|
-
.a1-system-banner__icon {
|
|
33
|
-
flex-shrink: 0;
|
|
34
|
-
display: flex;
|
|
35
|
-
font-size: var(--component-message-banner-icon-size);
|
|
36
|
-
line-height: 1;
|
|
37
|
-
--a1-icon-opsz: var(--component-message-banner-icon-optical-size);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/* ─── Content (title + body inline) ───────────────────────────────────────── */
|
|
41
|
-
|
|
42
|
-
.a1-system-banner__content {
|
|
43
|
-
flex: 1;
|
|
44
|
-
min-width: 0;
|
|
45
|
-
display: flex;
|
|
46
|
-
flex-wrap: wrap;
|
|
47
|
-
align-items: baseline;
|
|
48
|
-
gap: 0 var(--base-spacing-8);
|
|
49
|
-
font-family: var(--component-paragraph-font-family);
|
|
50
|
-
font-size: var(--semantic-font-size-body-sm);
|
|
51
|
-
line-height: var(--semantic-font-line-height-body);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.a1-system-banner__title {
|
|
55
|
-
font-weight: var(--component-message-banner-title-font-weight);
|
|
56
|
-
color: var(--a1-sysbanner-fg);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.a1-system-banner__body {
|
|
60
|
-
font-weight: var(--semantic-font-weight-body);
|
|
61
|
-
color: var(--a1-sysbanner-fg);
|
|
62
|
-
opacity: 0.85;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/* ─── Action ───────────────────────────────────────────────────────────────── */
|
|
66
|
-
|
|
67
|
-
.a1-system-banner__action {
|
|
68
|
-
flex-shrink: 0;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/* ─── Dismiss ──────────────────────────────────────────────────────────────── */
|
|
72
|
-
|
|
73
|
-
.a1-system-banner__dismiss {
|
|
74
|
-
flex-shrink: 0;
|
|
75
|
-
margin-inline-start: var(--base-spacing-4);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/* ─── Link override on solid bg ────────────────────────────────────────────── */
|
|
79
|
-
|
|
80
|
-
.a1-system-banner .a1-link {
|
|
81
|
-
color: var(--a1-sysbanner-fg);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.a1-system-banner .a1-link:hover {
|
|
85
|
-
color: color-mix(in srgb, var(--a1-sysbanner-fg) 80%, transparent);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.a1-system-banner .a1-link:active {
|
|
89
|
-
color: color-mix(in srgb, var(--a1-sysbanner-fg) 65%, transparent);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* ─── Tertiary button override on solid bg ─────────────────────────────────── */
|
|
93
|
-
|
|
94
|
-
.a1-system-banner .a1-button--tertiary {
|
|
95
|
-
--a1-button-foreground: var(--a1-sysbanner-fg);
|
|
96
|
-
--a1-button-foreground-hover: var(--a1-sysbanner-fg);
|
|
97
|
-
--a1-button-foreground-pressed: var(--a1-sysbanner-fg);
|
|
98
|
-
--a1-button-background: transparent;
|
|
99
|
-
--a1-button-background-hover: color-mix(in srgb, var(--a1-sysbanner-fg) 12%, transparent);
|
|
100
|
-
--a1-button-background-pressed: color-mix(in srgb, var(--a1-sysbanner-fg) 20%, transparent);
|
|
101
|
-
--a1-button-border: color-mix(in srgb, var(--a1-sysbanner-fg) 40%, transparent);
|
|
102
|
-
--a1-button-border-hover: color-mix(in srgb, var(--a1-sysbanner-fg) 60%, transparent);
|
|
103
|
-
--a1-button-border-pressed: color-mix(in srgb, var(--a1-sysbanner-fg) 75%, transparent);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/* ─── Icon button (dismiss) override on solid bg ───────────────────────────── */
|
|
107
|
-
|
|
108
|
-
.a1-system-banner .a1-icon-button {
|
|
109
|
-
--a1-icon-button-foreground: var(--a1-sysbanner-fg);
|
|
110
|
-
--a1-icon-button-foreground-hover: var(--a1-sysbanner-fg);
|
|
111
|
-
--a1-icon-button-foreground-pressed: var(--a1-sysbanner-fg);
|
|
112
|
-
--a1-icon-button-background: transparent;
|
|
113
|
-
--a1-icon-button-background-hover: color-mix(in srgb, var(--a1-sysbanner-fg) 12%, transparent);
|
|
114
|
-
--a1-icon-button-background-pressed: color-mix(in srgb, var(--a1-sysbanner-fg) 20%, transparent);
|
|
115
|
-
--a1-icon-button-border: transparent;
|
|
116
|
-
--a1-icon-button-border-hover: transparent;
|
|
117
|
-
--a1-icon-button-border-pressed: transparent;
|
|
118
|
-
}
|