@gtivr4/a1-design-system-react 0.1.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.
Files changed (45) hide show
  1. package/README.md +32 -0
  2. package/package.json +40 -0
  3. package/src/color-scheme.css +213 -0
  4. package/src/components/button/Button.jsx +45 -0
  5. package/src/components/button/button.css +135 -0
  6. package/src/components/button-container/ButtonContainer.jsx +27 -0
  7. package/src/components/button-container/button-container.css +38 -0
  8. package/src/components/card/Card.jsx +29 -0
  9. package/src/components/card/card.css +37 -0
  10. package/src/components/dialog/Dialog.jsx +44 -0
  11. package/src/components/dialog/dialog.css +58 -0
  12. package/src/components/grid/Grid.jsx +77 -0
  13. package/src/components/grid/grid.css +86 -0
  14. package/src/components/heading/Heading.jsx +69 -0
  15. package/src/components/heading/heading.css +76 -0
  16. package/src/components/icon/Icon.jsx +32 -0
  17. package/src/components/icon/icon.css +10 -0
  18. package/src/components/icon-button/IconButton.jsx +34 -0
  19. package/src/components/icon-button/icon-button.css +196 -0
  20. package/src/components/inverse/Inverse.jsx +18 -0
  21. package/src/components/labels/Labels.jsx +29 -0
  22. package/src/components/link/Link.jsx +41 -0
  23. package/src/components/link/link.css +50 -0
  24. package/src/components/menu/Menu.jsx +45 -0
  25. package/src/components/menu/menu.css +45 -0
  26. package/src/components/message/Message.jsx +103 -0
  27. package/src/components/message/message.css +226 -0
  28. package/src/components/notification/Notification.jsx +55 -0
  29. package/src/components/notification/notification.css +69 -0
  30. package/src/components/page-layout/PageLayout.jsx +40 -0
  31. package/src/components/page-layout/page-layout.css +61 -0
  32. package/src/components/pagination/Pagination.jsx +64 -0
  33. package/src/components/pagination/pagination.css +85 -0
  34. package/src/components/paragraph/Paragraph.jsx +26 -0
  35. package/src/components/paragraph/paragraph.css +16 -0
  36. package/src/components/segmented-control/SegmentedControl.jsx +77 -0
  37. package/src/components/segmented-control/segmented.css +76 -0
  38. package/src/components/side-nav/SideNav.jsx +208 -0
  39. package/src/components/side-nav/scrim.css +17 -0
  40. package/src/components/side-nav/side-nav.css +283 -0
  41. package/src/components/tabs/Tabs.jsx +102 -0
  42. package/src/components/tabs/tabs.css +135 -0
  43. package/src/index.js +20 -0
  44. package/src/themes.css +186 -0
  45. package/src/utilities/spacing.css +230 -0
@@ -0,0 +1,41 @@
1
+ import "./link.css";
2
+ import { Icon } from "../icon/Icon.jsx";
3
+
4
+ const sizes = ["xs", "sm", "md", "lg", "xl"];
5
+ const weights = ["normal", "medium", "semibold", "bold"];
6
+ const iconPositions = ["start", "end"];
7
+
8
+ export function Link({
9
+ as: Component = "a",
10
+ size,
11
+ weight,
12
+ icon,
13
+ iconPosition = "start",
14
+ className = "",
15
+ children,
16
+ ...props
17
+ }) {
18
+ const resolvedSize = sizes.includes(size) ? size : null;
19
+ const resolvedWeight = weights.includes(weight) ? weight : null;
20
+ const resolvedPosition = iconPositions.includes(iconPosition) ? iconPosition : "start";
21
+
22
+ const classes = [
23
+ "a1-link",
24
+ resolvedSize && `a1-link--${resolvedSize}`,
25
+ resolvedWeight && `a1-link--${resolvedWeight}`,
26
+ icon && "a1-link--has-icon",
27
+ className
28
+ ]
29
+ .filter(Boolean)
30
+ .join(" ");
31
+
32
+ const iconEl = icon ? <Icon name={icon} className="a1-link__icon" /> : null;
33
+
34
+ return (
35
+ <Component className={classes} {...props}>
36
+ {resolvedPosition === "start" && iconEl}
37
+ <span className="a1-link__text">{children}</span>
38
+ {resolvedPosition === "end" && iconEl}
39
+ </Component>
40
+ );
41
+ }
@@ -0,0 +1,50 @@
1
+ .a1-link {
2
+ color: var(--component-link-color);
3
+ text-decoration: none;
4
+ cursor: pointer;
5
+ font-family: var(--component-paragraph-font-family);
6
+ /* font-size and font-weight inherit from parent by default */
7
+ }
8
+
9
+ .a1-link:hover { color: var(--component-link-color-hover); }
10
+ .a1-link:active { color: var(--component-link-color-pressed); }
11
+
12
+ .a1-link:focus-visible {
13
+ outline: var(--component-link-focus-ring-width) solid var(--component-link-color);
14
+ outline-offset: var(--component-link-focus-ring-offset);
15
+ border-radius: var(--component-link-focus-ring-radius);
16
+ }
17
+
18
+ .a1-link__text {
19
+ text-decoration: underline;
20
+ text-decoration-color: currentColor;
21
+ text-underline-offset: var(--component-link-underline-offset);
22
+ }
23
+
24
+ /* ── Size modifiers ─────────────────────────────────────────────────────── */
25
+
26
+ .a1-link--xs { font-size: var(--semantic-font-size-body-xs); }
27
+ .a1-link--sm { font-size: var(--semantic-font-size-body-sm); }
28
+ .a1-link--md { font-size: var(--semantic-font-size-body-md); }
29
+ .a1-link--lg { font-size: var(--semantic-font-size-body-lg); }
30
+ .a1-link--xl { font-size: var(--semantic-font-size-body-xl); }
31
+
32
+ /* ── Weight modifiers ───────────────────────────────────────────────────── */
33
+
34
+ .a1-link--normal { font-weight: var(--base-font-weight-regular); --a1-icon-weight: var(--base-font-weight-regular); }
35
+ .a1-link--medium { font-weight: var(--base-font-weight-medium); --a1-icon-weight: var(--base-font-weight-medium); }
36
+ .a1-link--semibold { font-weight: var(--base-font-weight-semibold); --a1-icon-weight: var(--base-font-weight-semibold); }
37
+ .a1-link--bold { font-weight: var(--base-font-weight-bold); --a1-icon-weight: var(--base-font-weight-bold); }
38
+
39
+ /* ── Icon layout ────────────────────────────────────────────────────────── */
40
+
41
+ .a1-link--has-icon {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ gap: var(--component-link-icon-gap);
45
+ }
46
+
47
+ .a1-link__icon {
48
+ font-size: 1em;
49
+ flex-shrink: 0;
50
+ }
@@ -0,0 +1,45 @@
1
+ import { useEffect, useRef } from "react";
2
+ import "./menu.css";
3
+
4
+ export function Menu({ open, onClose, "aria-label": ariaLabel, children }) {
5
+ const ref = useRef(null);
6
+
7
+ useEffect(() => {
8
+ const el = ref.current;
9
+ if (!el) return;
10
+ if (open) {
11
+ if (!el.open) el.showModal();
12
+ } else if (el.open) {
13
+ el.close();
14
+ }
15
+ }, [open]);
16
+
17
+ useEffect(() => {
18
+ const el = ref.current;
19
+ if (!el) return;
20
+ const onCancel = (e) => { e.preventDefault(); onClose?.(); };
21
+ // Backdrop clicks hit the dialog element directly
22
+ const onClick = (e) => { if (e.target === el) onClose?.(); };
23
+ el.addEventListener("cancel", onCancel);
24
+ el.addEventListener("click", onClick);
25
+ return () => {
26
+ el.removeEventListener("cancel", onCancel);
27
+ el.removeEventListener("click", onClick);
28
+ };
29
+ }, [onClose]);
30
+
31
+ return (
32
+ <dialog ref={ref} className="a1-menu" aria-label={ariaLabel}>
33
+ {children}
34
+ </dialog>
35
+ );
36
+ }
37
+
38
+ export function MenuSection({ label, children }) {
39
+ return (
40
+ <div className="a1-menu__section">
41
+ {label && <p className="a1-menu__section-label">{label}</p>}
42
+ {children}
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,45 @@
1
+ .a1-menu {
2
+ /* Override browser centering; place below the header on the right */
3
+ position: fixed;
4
+ top: 72px; /* 64px header + 8px gap */
5
+ right: var(--base-spacing-16);
6
+ bottom: auto;
7
+ left: auto;
8
+ margin: 0;
9
+
10
+ width: 260px;
11
+ max-height: calc(100dvh - 88px);
12
+ overflow-y: auto;
13
+
14
+ padding: var(--base-spacing-20);
15
+ border: 1px solid var(--semantic-color-border-subtle);
16
+ border-radius: var(--base-radius-lg);
17
+ background: var(--semantic-color-surface-panel);
18
+ box-shadow: var(--semantic-shadow-lg);
19
+ color: var(--semantic-color-text-default);
20
+ }
21
+
22
+ .a1-menu::backdrop {
23
+ background: color-mix(in srgb, var(--base-color-neutral-900) 30%, transparent);
24
+ }
25
+
26
+ .a1-menu__section {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: var(--base-spacing-8);
30
+ }
31
+
32
+ .a1-menu__section + .a1-menu__section {
33
+ margin-top: var(--base-spacing-16);
34
+ padding-top: var(--base-spacing-16);
35
+ border-top: 1px solid var(--semantic-color-border-subtle);
36
+ }
37
+
38
+ .a1-menu__section-label {
39
+ margin: 0;
40
+ color: var(--semantic-color-text-muted);
41
+ font-size: var(--semantic-font-size-body-xs);
42
+ font-weight: 700;
43
+ text-transform: uppercase;
44
+ letter-spacing: 0.06em;
45
+ }
@@ -0,0 +1,103 @@
1
+ import "./message.css";
2
+ import { Icon } from "../icon/Icon.jsx";
3
+ import { IconButton } from "../icon-button/IconButton.jsx";
4
+
5
+ const STATUS_ICONS = {
6
+ neutral: "info",
7
+ info: "info",
8
+ success: "check_circle",
9
+ warn: "warning",
10
+ error: "error",
11
+ };
12
+
13
+ const STATUSES = ["neutral", "info", "success", "warn", "error"];
14
+ const ES_SCALES = ["page", "section", "card"];
15
+
16
+ /* ═══════════════════════════════════════════════════════════════════════════
17
+ MessageBanner
18
+ ═══════════════════════════════════════════════════════════════════════════ */
19
+
20
+ export function MessageBanner({
21
+ status = "neutral",
22
+ title,
23
+ icon,
24
+ onDismiss,
25
+ children,
26
+ }) {
27
+ const resolvedStatus = STATUSES.includes(status) ? status : "neutral";
28
+ const resolvedIcon = icon ?? STATUS_ICONS[resolvedStatus];
29
+
30
+ return (
31
+ <div
32
+ className={`a1-message-banner a1-message-banner--${resolvedStatus}`}
33
+ role="alert"
34
+ aria-live="polite"
35
+ >
36
+ <span className="a1-message-banner__icon" aria-hidden="true">
37
+ <Icon name={resolvedIcon} />
38
+ </span>
39
+
40
+ <div className="a1-message-banner__content">
41
+ {title && <p className="a1-message-banner__title">{title}</p>}
42
+ {children && <p className="a1-message-banner__body">{children}</p>}
43
+ </div>
44
+
45
+ {onDismiss && (
46
+ <IconButton
47
+ icon="close"
48
+ label="Dismiss"
49
+ onClick={onDismiss}
50
+ className="a1-message-banner__dismiss"
51
+ />
52
+ )}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ /* ═══════════════════════════════════════════════════════════════════════════
58
+ MessageBadge (inline filled status chip)
59
+ ═══════════════════════════════════════════════════════════════════════════ */
60
+
61
+ const VARIANTS = ["bold", "subtle"];
62
+
63
+ export function MessageBadge({ status = "neutral", variant = "bold", icon, children }) {
64
+ const resolvedStatus = STATUSES.includes(status) ? status : "neutral";
65
+ const resolvedVariant = VARIANTS.includes(variant) ? variant : "bold";
66
+ const resolvedIcon = icon ?? STATUS_ICONS[resolvedStatus];
67
+
68
+ return (
69
+ <span className={[
70
+ "a1-message-badge",
71
+ `a1-message-badge--${resolvedStatus}`,
72
+ resolvedVariant === "subtle" && "a1-message-badge--subtle",
73
+ ].filter(Boolean).join(" ")}>
74
+ <Icon name={resolvedIcon} />
75
+ {children}
76
+ </span>
77
+ );
78
+ }
79
+
80
+ /* ═══════════════════════════════════════════════════════════════════════════
81
+ MessageEmptyState
82
+ ═══════════════════════════════════════════════════════════════════════════ */
83
+
84
+ export function MessageEmptyState({
85
+ scale = "section",
86
+ icon = "inbox",
87
+ title,
88
+ description,
89
+ action,
90
+ }) {
91
+ const resolvedScale = ES_SCALES.includes(scale) ? scale : "section";
92
+
93
+ return (
94
+ <div className={`a1-message-empty a1-message-empty--${resolvedScale}`}>
95
+ <div className="a1-message-empty__icon-wrap" aria-hidden="true">
96
+ <Icon name={icon} />
97
+ </div>
98
+ {title && <p className="a1-message-empty__title">{title}</p>}
99
+ {description && <p className="a1-message-empty__description">{description}</p>}
100
+ {action && <div className="a1-message-empty__action">{action}</div>}
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,226 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════
2
+ MessageBanner
3
+ ═══════════════════════════════════════════════════════════════════════════ */
4
+
5
+ .a1-message-banner {
6
+ /* defaults: neutral */
7
+ --a1-msg-surface: var(--semantic-color-surface-panel);
8
+ --a1-msg-border: var(--semantic-color-border-subtle);
9
+ --a1-msg-accent: var(--semantic-color-text-muted);
10
+
11
+ display: flex;
12
+ align-items: flex-start;
13
+ gap: var(--component-message-banner-padding);
14
+ padding: var(--component-message-banner-padding);
15
+ border: var(--component-message-banner-border-width) solid var(--a1-msg-border);
16
+ border-radius: var(--component-message-banner-border-radius);
17
+ background: var(--a1-msg-surface);
18
+ }
19
+
20
+ .a1-message-banner--info {
21
+ --a1-msg-surface: var(--semantic-color-status-info-surface);
22
+ --a1-msg-border: var(--semantic-color-status-info-border);
23
+ --a1-msg-accent: var(--semantic-color-status-info-background);
24
+ }
25
+ .a1-message-banner--success {
26
+ --a1-msg-surface: var(--semantic-color-status-success-surface);
27
+ --a1-msg-border: var(--semantic-color-status-success-border);
28
+ --a1-msg-accent: var(--semantic-color-status-success-background);
29
+ }
30
+ .a1-message-banner--warn {
31
+ --a1-msg-surface: var(--semantic-color-status-warn-surface);
32
+ --a1-msg-border: var(--semantic-color-status-warn-border);
33
+ --a1-msg-accent: var(--semantic-color-status-warn-background);
34
+ }
35
+ .a1-message-banner--error {
36
+ --a1-msg-surface: var(--semantic-color-status-error-surface);
37
+ --a1-msg-border: var(--semantic-color-status-error-border);
38
+ --a1-msg-accent: var(--semantic-color-status-error-background);
39
+ }
40
+
41
+ .a1-message-banner__icon {
42
+ flex-shrink: 0;
43
+ color: var(--a1-msg-accent);
44
+ font-size: var(--component-message-banner-icon-size);
45
+ line-height: 1;
46
+ margin-top: var(--component-message-banner-icon-margin-top);
47
+ --a1-icon-opsz: var(--component-message-banner-icon-optical-size);
48
+ }
49
+
50
+ .a1-message-banner__content {
51
+ flex: 1;
52
+ min-width: 0;
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: var(--base-spacing-4);
56
+ }
57
+
58
+ .a1-message-banner__title {
59
+ margin: 0;
60
+ font-family: var(--component-paragraph-font-family);
61
+ font-size: var(--semantic-font-size-body-sm);
62
+ font-weight: var(--component-message-banner-title-font-weight);
63
+ color: var(--semantic-color-text-default);
64
+ line-height: var(--semantic-font-line-height-heading);
65
+ }
66
+
67
+ .a1-message-banner__body {
68
+ margin: 0;
69
+ font-family: var(--component-paragraph-font-family);
70
+ font-size: var(--semantic-font-size-body-sm);
71
+ font-weight: var(--semantic-font-weight-body);
72
+ color: var(--semantic-color-text-muted);
73
+ line-height: var(--semantic-font-line-height-body);
74
+ }
75
+
76
+ .a1-message-banner__dismiss {
77
+ flex-shrink: 0;
78
+ margin-top: var(--component-message-banner-dismiss-offset);
79
+ margin-right: var(--component-message-banner-dismiss-offset);
80
+ }
81
+
82
+ /* ═══════════════════════════════════════════════════════════════════════════
83
+ MessageBadge (inline status chip — filled)
84
+ ═══════════════════════════════════════════════════════════════════════════ */
85
+
86
+ .a1-message-badge {
87
+ /* defaults: neutral */
88
+ --a1-msg-accent: var(--semantic-color-action-background);
89
+ --a1-msg-foreground: var(--semantic-color-action-foreground);
90
+
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: var(--base-spacing-4);
94
+ padding: var(--component-message-badge-padding-block)
95
+ var(--component-message-badge-padding-inline);
96
+ border-radius: var(--component-message-badge-border-radius);
97
+ background: var(--a1-msg-accent);
98
+ color: var(--a1-msg-foreground);
99
+ font-family: var(--component-paragraph-font-family);
100
+ font-size: var(--semantic-font-size-body-sm);
101
+ font-weight: var(--component-message-badge-font-weight);
102
+ line-height: 1;
103
+ white-space: nowrap;
104
+ }
105
+
106
+ .a1-message-badge--info {
107
+ --a1-msg-accent: var(--semantic-color-status-info-background);
108
+ --a1-msg-foreground: var(--semantic-color-status-info-foreground);
109
+ }
110
+ .a1-message-badge--success {
111
+ --a1-msg-accent: var(--semantic-color-status-success-background);
112
+ --a1-msg-foreground: var(--semantic-color-status-success-foreground);
113
+ }
114
+ .a1-message-badge--warn {
115
+ --a1-msg-accent: var(--semantic-color-status-warn-background);
116
+ --a1-msg-foreground: var(--semantic-color-status-warn-foreground);
117
+ }
118
+ .a1-message-badge--error {
119
+ --a1-msg-accent: var(--semantic-color-status-error-background);
120
+ --a1-msg-foreground: var(--semantic-color-status-error-foreground);
121
+ }
122
+
123
+ /* Subtle variant — light surface + border, no fill */
124
+ .a1-message-badge--subtle {
125
+ --a1-msg-accent: var(--semantic-color-action-surface);
126
+ --a1-msg-foreground: var(--semantic-color-action-background);
127
+ border: 1px solid var(--semantic-color-action-border);
128
+ }
129
+
130
+ .a1-message-badge .a1-icon {
131
+ font-size: 1em;
132
+ }
133
+
134
+ /* ═══════════════════════════════════════════════════════════════════════════
135
+ MessageEmptyState
136
+ ═══════════════════════════════════════════════════════════════════════════ */
137
+
138
+ .a1-message-empty {
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: center;
142
+ text-align: center;
143
+ }
144
+
145
+ /* ── Scale: page ────────────────────────────────────────────────────────── */
146
+
147
+ .a1-message-empty--page {
148
+ --a1-empty-icon: var(--component-message-empty-state-icon-size-page);
149
+ --a1-empty-wrap: var(--component-message-empty-state-wrap-size-page);
150
+ --a1-icon-opsz: var(--component-message-empty-state-icon-optical-size-page);
151
+ gap: var(--base-spacing-16);
152
+ padding: var(--base-spacing-64) var(--base-spacing-40);
153
+ max-width: var(--component-message-empty-state-max-width-page);
154
+ margin-inline: auto;
155
+ }
156
+
157
+ /* ── Scale: section ─────────────────────────────────────────────────────── */
158
+
159
+ .a1-message-empty--section {
160
+ --a1-empty-icon: var(--component-message-empty-state-icon-size-section);
161
+ --a1-empty-wrap: var(--component-message-empty-state-wrap-size-section);
162
+ --a1-icon-opsz: var(--component-message-empty-state-icon-optical-size-section);
163
+ gap: var(--base-spacing-12);
164
+ padding: var(--base-spacing-40) var(--base-spacing-24);
165
+ max-width: var(--component-message-empty-state-max-width-section);
166
+ margin-inline: auto;
167
+ }
168
+
169
+ /* ── Scale: card ────────────────────────────────────────────────────────── */
170
+
171
+ .a1-message-empty--card {
172
+ --a1-empty-icon: var(--component-message-empty-state-icon-size-card);
173
+ --a1-empty-wrap: var(--component-message-empty-state-wrap-size-card);
174
+ --a1-icon-opsz: var(--component-message-empty-state-icon-optical-size-card);
175
+ gap: var(--base-spacing-8);
176
+ padding: var(--base-spacing-16);
177
+ }
178
+
179
+ /* ── Icon wrap ──────────────────────────────────────────────────────────── */
180
+
181
+ .a1-message-empty__icon-wrap {
182
+ flex-shrink: 0;
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ width: var(--a1-empty-wrap);
187
+ height: var(--a1-empty-wrap);
188
+ border-radius: 50%;
189
+ background: var(--semantic-color-surface-raised);
190
+ color: var(--semantic-color-text-muted);
191
+ }
192
+
193
+ .a1-message-empty--card .a1-message-empty__icon-wrap {
194
+ background: transparent;
195
+ }
196
+
197
+ .a1-message-empty__icon-wrap .a1-icon {
198
+ font-size: var(--a1-empty-icon);
199
+ }
200
+
201
+ /* ── Text ───────────────────────────────────────────────────────────────── */
202
+
203
+ .a1-message-empty__title {
204
+ margin: 0;
205
+ font-family: var(--component-heading-font-family-heading);
206
+ font-weight: var(--component-heading-font-weight-heading);
207
+ color: var(--semantic-color-text-default);
208
+ }
209
+
210
+ .a1-message-empty--page .a1-message-empty__title { font-size: var(--semantic-font-size-heading-sm); }
211
+ .a1-message-empty--section .a1-message-empty__title { font-size: var(--semantic-font-size-heading-xs); }
212
+ .a1-message-empty--card .a1-message-empty__title { font-size: var(--semantic-font-size-body-sm); }
213
+
214
+ .a1-message-empty__description {
215
+ margin: 0;
216
+ font-family: var(--component-paragraph-font-family);
217
+ color: var(--semantic-color-text-muted);
218
+ }
219
+
220
+ .a1-message-empty--page .a1-message-empty__description { font-size: var(--semantic-font-size-body-md); }
221
+ .a1-message-empty--section .a1-message-empty__description { font-size: var(--semantic-font-size-body-sm); }
222
+ .a1-message-empty--card .a1-message-empty__description { font-size: var(--semantic-font-size-body-xs); }
223
+
224
+ .a1-message-empty__action {
225
+ margin-top: var(--base-spacing-4);
226
+ }
@@ -0,0 +1,55 @@
1
+ import "./notification.css";
2
+
3
+ const variants = ["default", "error", "success", "warn", "info"];
4
+ const positions = ["top-right", "top-left", "bottom-right", "bottom-left"];
5
+
6
+ function formatCount(n, max) {
7
+ if (n >= 1_000_000) return +(n / 1_000_000).toFixed(1) + "M";
8
+ if (n >= 1_000) return +(n / 1_000).toFixed(1) + "k";
9
+ if (n > max) return `${max}+`;
10
+ return String(n);
11
+ }
12
+
13
+ export function Notification({
14
+ children,
15
+ count,
16
+ label,
17
+ dot = false,
18
+ variant = "default",
19
+ position = "top-right",
20
+ max = 99,
21
+ }) {
22
+ const resolvedVariant = variants.includes(variant) ? variant : "default";
23
+ const resolvedPosition = positions.includes(position) ? position : "top-right";
24
+
25
+ const isDot = dot || (count === undefined && label === undefined);
26
+
27
+ let content = null;
28
+ if (!isDot) {
29
+ if (count !== undefined) {
30
+ content = formatCount(count, max);
31
+ } else {
32
+ content = label;
33
+ }
34
+ }
35
+
36
+ const classes = [
37
+ "a1-notification",
38
+ `a1-notification--${resolvedVariant}`,
39
+ `a1-notification--${resolvedPosition}`,
40
+ isDot && "a1-notification--dot",
41
+ ]
42
+ .filter(Boolean)
43
+ .join(" ");
44
+
45
+ const ariaLabel = count !== undefined ? `${count} notifications` : undefined;
46
+
47
+ return (
48
+ <span className="a1-notification-root">
49
+ {children}
50
+ <span className={classes} aria-label={ariaLabel}>
51
+ {content}
52
+ </span>
53
+ </span>
54
+ );
55
+ }
@@ -0,0 +1,69 @@
1
+ .a1-notification-root {
2
+ position: relative;
3
+ display: inline-flex;
4
+ }
5
+
6
+ .a1-notification {
7
+ position: absolute;
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ box-sizing: border-box;
12
+ min-width: var(--component-notification-height);
13
+ height: var(--component-notification-height);
14
+ padding: 0 var(--component-notification-padding-inline);
15
+ border-radius: calc(var(--component-notification-height) / 2);
16
+ font-family: var(--component-paragraph-font-family);
17
+ font-size: var(--component-notification-font-size);
18
+ font-weight: var(--component-notification-font-weight);
19
+ line-height: 1;
20
+ white-space: nowrap;
21
+ background: var(--a1-notification-background);
22
+ color: var(--a1-notification-foreground);
23
+ /* Ring separates the notification from its underlying element */
24
+ box-shadow: 0 0 0 var(--component-notification-ring-width) var(--semantic-color-surface-page);
25
+ }
26
+
27
+ /* ── Position ───────────────────────────────────────────────────────────── */
28
+
29
+ .a1-notification--top-right { top: 0; right: 0; transform: translate(50%, -50%); }
30
+ .a1-notification--top-left { top: 0; left: 0; transform: translate(-50%, -50%); }
31
+ .a1-notification--bottom-right { bottom: 0; right: 0; transform: translate(50%, 50%); }
32
+ .a1-notification--bottom-left { bottom: 0; left: 0; transform: translate(-50%, 50%); }
33
+
34
+ /* ── Dot ────────────────────────────────────────────────────────────────── */
35
+
36
+ .a1-notification--dot {
37
+ min-width: var(--component-notification-dot-size);
38
+ width: var(--component-notification-dot-size);
39
+ height: var(--component-notification-dot-size);
40
+ padding: 0;
41
+ border-radius: 50%;
42
+ }
43
+
44
+ /* ── Variants ───────────────────────────────────────────────────────────── */
45
+
46
+ .a1-notification--default {
47
+ --a1-notification-background: var(--base-color-neutral-600);
48
+ --a1-notification-foreground: var(--base-color-neutral-0);
49
+ }
50
+
51
+ .a1-notification--error {
52
+ --a1-notification-background: var(--semantic-color-status-error-background);
53
+ --a1-notification-foreground: var(--semantic-color-status-error-foreground);
54
+ }
55
+
56
+ .a1-notification--success {
57
+ --a1-notification-background: var(--semantic-color-status-success-background);
58
+ --a1-notification-foreground: var(--semantic-color-status-success-foreground);
59
+ }
60
+
61
+ .a1-notification--warn {
62
+ --a1-notification-background: var(--semantic-color-status-warn-background);
63
+ --a1-notification-foreground: var(--semantic-color-status-warn-foreground);
64
+ }
65
+
66
+ .a1-notification--info {
67
+ --a1-notification-background: var(--semantic-color-status-info-background);
68
+ --a1-notification-foreground: var(--semantic-color-status-info-foreground);
69
+ }
@@ -0,0 +1,40 @@
1
+ import "./page-layout.css";
2
+
3
+ export function PageLayout({
4
+ header,
5
+ footer,
6
+ sidebar,
7
+ sidebarPlacement = "start",
8
+ stickyHeader = false,
9
+ className = "",
10
+ children,
11
+ ...props
12
+ }) {
13
+ const rootClasses = [
14
+ "a1-page-layout",
15
+ stickyHeader && "a1-page-layout--sticky-header",
16
+ sidebar && `a1-page-layout--sidebar-${sidebarPlacement}`,
17
+ className,
18
+ ]
19
+ .filter(Boolean)
20
+ .join(" ");
21
+
22
+ return (
23
+ <div className={rootClasses} {...props}>
24
+ {header && (
25
+ <header className="a1-page-layout__header">{header}</header>
26
+ )}
27
+
28
+ <div className="a1-page-layout__body">
29
+ {sidebar && (
30
+ <aside className="a1-page-layout__sidebar">{sidebar}</aside>
31
+ )}
32
+ <main className="a1-page-layout__main">{children}</main>
33
+ </div>
34
+
35
+ {footer && (
36
+ <footer className="a1-page-layout__footer">{footer}</footer>
37
+ )}
38
+ </div>
39
+ );
40
+ }