@gtivr4/a1-design-system-react 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtivr4/a1-design-system-react",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "React components for the A1 token-driven design system.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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-page);
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);
@@ -17,9 +17,9 @@ const BP_QUERIES = {
17
17
  };
18
18
 
19
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";
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
23
  // Cascade xs → sm → md → lg → xl, carrying forward the last explicit value.
24
24
  let resolved = prop.xs ?? "start";
25
25
  for (const [bp, query] of Object.entries(BP_QUERIES)) {
@@ -27,7 +27,7 @@ function resolveIconAbove(prop) {
27
27
  resolved = prop[bp] ?? resolved;
28
28
  }
29
29
  }
30
- return resolved === "above";
30
+ return resolved; // "start" | "above" | "hidden"
31
31
  }
32
32
 
33
33
  // Split a flat items array into sections separated by { divider: true } markers.
@@ -589,7 +589,7 @@ export function TopHeader({
589
589
  navIconPosition = "start",
590
590
  className = "",
591
591
  }) {
592
- const [iconAbove, setIconAbove] = useState(() => resolveIconAbove(navIconPosition));
592
+ const [navMode, setNavMode] = useState(() => resolveNavMode(navIconPosition));
593
593
  const [openSubmenu, setOpenSubmenu] = useState(null);
594
594
  const [openAction, setOpenAction] = useState(null);
595
595
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
@@ -617,11 +617,11 @@ export function TopHeader({
617
617
  return () => desktopQuery.removeListener(closeAtDesktop);
618
618
  }, [mobileNavOpen]);
619
619
 
620
- // Re-resolve iconAbove whenever navIconPosition or the viewport changes.
620
+ // Re-resolve navMode whenever navIconPosition or the viewport changes.
621
621
  useEffect(() => {
622
622
  if (typeof window === "undefined" || !window.matchMedia) return undefined;
623
623
 
624
- const update = () => setIconAbove(resolveIconAbove(navIconPosition));
624
+ const update = () => setNavMode(resolveNavMode(navIconPosition));
625
625
  const listeners = Object.values(BP_QUERIES).map((q) => {
626
626
  const mq = window.matchMedia(q);
627
627
  mq.addEventListener("change", update);
@@ -637,7 +637,8 @@ export function TopHeader({
637
637
  <header
638
638
  className={[
639
639
  "a1-top-header",
640
- iconAbove && "a1-top-header--nav-icon-above",
640
+ navMode === "above" && "a1-top-header--nav-icon-above",
641
+ navMode === "hidden" && "a1-top-header--nav-hidden",
641
642
  className,
642
643
  ].filter(Boolean).join(" ")}
643
644
  >
@@ -666,7 +667,7 @@ export function TopHeader({
666
667
  item={item}
667
668
  openId={openSubmenu}
668
669
  onOpen={setOpenSubmenu}
669
- iconAbove={iconAbove}
670
+ iconAbove={navMode === "above"}
670
671
  />
671
672
  ))}
672
673
  </ul>
@@ -698,7 +699,7 @@ export function TopHeader({
698
699
  </div>
699
700
  </header>
700
701
 
701
- {mobileNavOpen && (
702
+ {mobileNavOpen && navMode !== "hidden" && (
702
703
  <MobileDrawer
703
704
  navItems={navItems}
704
705
  onClose={() => setMobileNavOpen(false)}
@@ -322,21 +322,27 @@
322
322
 
323
323
  /* ── Icon-above nav layout ────────────────────────────────────────────────── */
324
324
 
325
- /* Nav links become a column: icon above, label (+ optional chevron) below. */
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. */
326
328
  .a1-top-header--nav-icon-above .a1-top-header__nav-link {
327
329
  flex-direction: column;
328
330
  align-items: center;
329
- gap: var(--base-spacing-2);
331
+ gap: var(--a1-nav-stacked-gap, var(--base-spacing-2));
330
332
  padding-block: var(--base-spacing-8);
331
333
  padding-inline: var(--base-spacing-8);
332
- font-size: var(--semantic-font-size-body-xs);
334
+ font-size: var(--a1-nav-stacked-label-size, var(--semantic-font-size-body-xs));
333
335
  }
334
336
 
335
337
  /* Icon slightly larger so it reads clearly at the top of each item. */
336
338
  .a1-top-header--nav-icon-above .a1-top-header__nav-link-icon {
337
- font-size: var(--semantic-font-size-heading-sm);
339
+ font-size: var(--a1-nav-stacked-icon-size, var(--semantic-font-size-heading-sm));
338
340
  }
339
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
+
340
346
  /* Label row: inline-flex so the small chevron sits beside the text. */
341
347
  .a1-top-header--nav-icon-above .a1-top-header__nav-link-label {
342
348
  display: inline-flex;
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,6 +127,7 @@
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;
@@ -181,6 +182,7 @@
181
182
  --semantic-color-text-inverse: #ffffff;
182
183
  --semantic-color-text-accent: #7c3aed;
183
184
  --semantic-color-surface-page: #ffffff;
185
+ --semantic-color-surface-card: #ffffff;
184
186
  --semantic-color-surface-panel: #f0f6fe;
185
187
  --semantic-color-surface-raised: #e1e8f3;
186
188
  --semantic-color-surface-inverse: #060b14;
@@ -333,6 +335,9 @@
333
335
  --component-blockquote-padding-inline: 1.5rem;
334
336
  --component-blockquote-mark-size: 5rem;
335
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;
336
341
  --component-calendar-month-gap: 2rem;
337
342
  --component-calendar-heading-padding-block: 0.75rem;
338
343
  --component-calendar-heading-padding-block-compact: 0.5rem;