@gtivr4/a1-design-system-react 0.12.0 → 0.13.3

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 (65) hide show
  1. package/package.json +1 -1
  2. package/src/components/accordion/Accordion.jsx +2 -0
  3. package/src/components/banner/Banner.jsx +4 -1
  4. package/src/components/blockquote/blockquote.css +0 -2
  5. package/src/components/bottom-drawer/BottomDrawer.jsx +2 -2
  6. package/src/components/button/Button.d.ts +4 -0
  7. package/src/components/button/Button.jsx +15 -3
  8. package/src/components/button/button.css +39 -0
  9. package/src/components/calendar/calendar.css +0 -2
  10. package/src/components/card/card.css +1 -0
  11. package/src/components/checkbox-group/CheckboxGroup.jsx +1 -1
  12. package/src/components/checkbox-group/checkbox-group.css +3 -3
  13. package/src/components/choice-group/ChoiceGroup.d.ts +23 -0
  14. package/src/components/choice-group/ChoiceGroup.jsx +22 -10
  15. package/src/components/choice-group/choice-group.css +53 -8
  16. package/src/components/code/Code.d.ts +4 -0
  17. package/src/components/code/Code.jsx +44 -8
  18. package/src/components/code/code.css +29 -0
  19. package/src/components/context-menu/ContextMenu.d.ts +56 -0
  20. package/src/components/context-menu/ContextMenu.jsx +146 -0
  21. package/src/components/context-menu/context-menu.css +107 -0
  22. package/src/components/data-table/DataTable.jsx +1 -1
  23. package/src/components/definition-list/definition-list.css +15 -0
  24. package/src/components/divider/Divider.d.ts +4 -2
  25. package/src/components/divider/Divider.jsx +6 -1
  26. package/src/components/divider/divider.css +9 -5
  27. package/src/components/field/DateField.jsx +17 -2
  28. package/src/components/field/SelectField.jsx +1 -1
  29. package/src/components/field/TextField.d.ts +2 -0
  30. package/src/components/field/TextField.jsx +1 -1
  31. package/src/components/field/TextareaField.jsx +1 -1
  32. package/src/components/field/TimeField.jsx +17 -2
  33. package/src/components/field/field.css +12 -5
  34. package/src/components/field/textarea-field.css +1 -2
  35. package/src/components/fieldset/fieldset.css +2 -0
  36. package/src/components/icon-button/IconButton.d.ts +8 -0
  37. package/src/components/icon-button/IconButton.jsx +9 -4
  38. package/src/components/inline-editable/InlineEditable.d.ts +25 -0
  39. package/src/components/inline-editable/InlineEditable.jsx +77 -1
  40. package/src/components/inline-editable/inline-editable.css +44 -1
  41. package/src/components/message/Message.jsx +15 -9
  42. package/src/components/page-layout/page-layout.css +13 -0
  43. package/src/components/page-nav/page-nav.css +0 -2
  44. package/src/components/pagination/Pagination.jsx +3 -1
  45. package/src/components/radio-group/RadioGroup.jsx +1 -1
  46. package/src/components/radio-group/radio-group.css +3 -3
  47. package/src/components/section/Section.d.ts +8 -0
  48. package/src/components/section/Section.jsx +24 -0
  49. package/src/components/section/section.css +28 -0
  50. package/src/components/snackbar/Snackbar.d.ts +24 -0
  51. package/src/components/snackbar/Snackbar.jsx +11 -8
  52. package/src/components/snackbar/snackbar.css +7 -22
  53. package/src/components/stack/Stack.jsx +2 -1
  54. package/src/components/sticky-actions/StickyActions.d.ts +7 -0
  55. package/src/components/sticky-actions/StickyActions.jsx +23 -4
  56. package/src/components/sticky-actions/sticky-actions.css +5 -3
  57. package/src/components/tabs/Tabs.d.ts +2 -0
  58. package/src/components/tabs/Tabs.jsx +3 -3
  59. package/src/components/tabs/tabs.css +95 -0
  60. package/src/components/top-header/TopHeader.jsx +2 -0
  61. package/src/components/tree-menu/TreeMenu.d.ts +54 -0
  62. package/src/components/tree-menu/TreeMenu.jsx +500 -0
  63. package/src/components/tree-menu/tree-menu.css +254 -0
  64. package/src/index.js +2 -0
  65. package/src/tokens.css +16 -0
@@ -1,7 +1,83 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import "./inline-editable.css";
3
3
 
4
- export function InlineEditable({
4
+ export function InlineEditable({ seamless = false, ...props }) {
5
+ // Seamless edits the text in place (contentEditable) so the surrounding
6
+ // component defines all typography; the boxed variant swaps to a field.
7
+ return seamless ? <SeamlessEditable {...props} /> : <BoxEditable {...props} />;
8
+ }
9
+
10
+ /* ── Seamless — edits in place, inheriting all surrounding text styling ────── */
11
+
12
+ function SeamlessEditable({
13
+ value,
14
+ onChange,
15
+ multiline = false,
16
+ disabled = false,
17
+ placeholder,
18
+ className = "",
19
+ inputClassName: _inputClassName,
20
+ children: _children,
21
+ ...props
22
+ }) {
23
+ const ref = useRef(null);
24
+
25
+ // Push the value into the DOM, but never while the user is actively typing
26
+ // here — overwriting the text node would reset the caret.
27
+ useEffect(() => {
28
+ const el = ref.current;
29
+ if (!el) return;
30
+ const text = value ?? "";
31
+ if (document.activeElement !== el && el.innerText !== text) {
32
+ el.innerText = text;
33
+ }
34
+ }, [value]);
35
+
36
+ function handleInput(event) {
37
+ onChange?.(event.currentTarget.innerText);
38
+ }
39
+
40
+ function handleKeyDown(event) {
41
+ if (event.key === "Escape") {
42
+ event.currentTarget.blur();
43
+ return;
44
+ }
45
+ if (!multiline && event.key === "Enter") {
46
+ event.preventDefault();
47
+ event.currentTarget.blur();
48
+ }
49
+ }
50
+
51
+ const classes = [
52
+ "a1-inline-editable",
53
+ "a1-inline-editable--seamless",
54
+ multiline && "a1-inline-editable--multiline",
55
+ disabled && "a1-inline-editable--disabled",
56
+ className,
57
+ ].filter(Boolean).join(" ");
58
+
59
+ return (
60
+ <span
61
+ {...props}
62
+ ref={ref}
63
+ className={classes}
64
+ contentEditable={!disabled}
65
+ suppressContentEditableWarning
66
+ role="textbox"
67
+ aria-multiline={multiline || undefined}
68
+ aria-label={props["aria-label"] ?? placeholder}
69
+ aria-disabled={disabled || undefined}
70
+ tabIndex={disabled ? undefined : 0}
71
+ data-placeholder={placeholder}
72
+ onInput={disabled ? undefined : handleInput}
73
+ onKeyDown={disabled ? undefined : handleKeyDown}
74
+ />
75
+ );
76
+ }
77
+
78
+ /* ── Boxed — click to reveal a field, commit on blur/Enter ─────────────────── */
79
+
80
+ function BoxEditable({
5
81
  value,
6
82
  onChange,
7
83
  multiline = false,
@@ -16,8 +16,14 @@
16
16
  outline-offset: 2px;
17
17
  }
18
18
 
19
+ /* Disabled — render as plain, selectable text with no interactive affordances */
19
20
  .a1-inline-editable--disabled {
20
- cursor: default;
21
+ cursor: auto;
22
+ user-select: text;
23
+ }
24
+
25
+ .a1-inline-editable--disabled:hover {
26
+ outline-color: transparent;
21
27
  }
22
28
 
23
29
  .a1-inline-editable__placeholder {
@@ -45,3 +51,40 @@
45
51
  outline: 2px solid var(--semantic-color-interactive-default);
46
52
  outline-offset: -1px;
47
53
  }
54
+
55
+ /* ── Seamless ──────────────────────────────────────────────────────────────
56
+ Edits the text in place via contentEditable. The element inherits everything
57
+ — font, size, weight, colour, line-height, alignment, wrapping — from the
58
+ surrounding component (Heading, Paragraph, Button, …), so editing never
59
+ resizes or restyles the text. Only a focus ring is added, for accessibility. */
60
+ .a1-inline-editable--seamless {
61
+ display: inline;
62
+ cursor: text;
63
+ border-radius: var(--base-border-radius-sm);
64
+ outline: none;
65
+ /* allow the caret/selection to sit just outside the glyphs without clipping */
66
+ outline-offset: 2px;
67
+ }
68
+
69
+ /* Multiline keeps authored line breaks */
70
+ .a1-inline-editable--multiline {
71
+ white-space: pre-wrap;
72
+ }
73
+
74
+ .a1-inline-editable--seamless:focus,
75
+ .a1-inline-editable--seamless:focus-visible {
76
+ outline: 2px solid var(--semantic-color-interactive-default);
77
+ }
78
+
79
+ /* Placeholder while empty (no value typed yet) */
80
+ .a1-inline-editable--seamless:empty::before {
81
+ content: attr(data-placeholder);
82
+ color: var(--semantic-color-text-muted);
83
+ opacity: 0.7;
84
+ }
85
+
86
+ /* Disabled seamless — plain, selectable text with no caret affordance */
87
+ .a1-inline-editable--seamless.a1-inline-editable--disabled {
88
+ cursor: auto;
89
+ user-select: text;
90
+ }
@@ -24,19 +24,23 @@ const ES_SCALE_CONFIG = {
24
24
  MessageBadge (inline filled status chip)
25
25
  ═══════════════════════════════════════════════════════════════════════════ */
26
26
 
27
- export function MessageBadge({ status = "neutral", subtle = false, size = "md", icon, children }) {
27
+ export function MessageBadge({ status = "neutral", subtle = false, size = "md", icon, className = "", children, ...rest }) {
28
28
  const resolvedStatus = STATUSES.includes(status) ? status : "neutral";
29
29
  // icon={null} explicitly suppresses the icon; undefined falls back to the status default
30
30
  const resolvedIcon = icon === null ? null : (icon ?? STATUS_ICONS[resolvedStatus]);
31
31
 
32
32
  return (
33
- <span className={[
34
- "a1-message-badge",
35
- `a1-message-badge--${resolvedStatus}`,
36
- subtle && "a1-message-badge--subtle",
37
- size === "sm" && "a1-message-badge--sm",
38
- size === "lg" && "a1-message-badge--lg",
39
- ].filter(Boolean).join(" ")}>
33
+ <span
34
+ className={[
35
+ "a1-message-badge",
36
+ `a1-message-badge--${resolvedStatus}`,
37
+ subtle && "a1-message-badge--subtle",
38
+ size === "sm" && "a1-message-badge--sm",
39
+ size === "lg" && "a1-message-badge--lg",
40
+ className,
41
+ ].filter(Boolean).join(" ")}
42
+ {...rest}
43
+ >
40
44
  {resolvedIcon && <Icon name={resolvedIcon} />}
41
45
  {children}
42
46
  </span>
@@ -53,12 +57,14 @@ export function MessageEmptyState({
53
57
  title,
54
58
  description,
55
59
  action,
60
+ className = "",
61
+ ...rest
56
62
  }) {
57
63
  const resolvedScale = ES_SCALES.includes(scale) ? scale : "section";
58
64
  const { headingAs, headingSize, paragraphSize } = ES_SCALE_CONFIG[resolvedScale];
59
65
 
60
66
  return (
61
- <div className={`a1-message-empty a1-message-empty--${resolvedScale}`}>
67
+ <div className={`a1-message-empty a1-message-empty--${resolvedScale}${className ? ` ${className}` : ""}`} {...rest}>
62
68
  <div className="a1-message-empty__icon-wrap" aria-hidden="true">
63
69
  <Icon name={icon} />
64
70
  </div>
@@ -110,6 +110,19 @@
110
110
  overflow-y: auto;
111
111
  }
112
112
 
113
+ /* When a SideNav is inside the viewport-height sidebar, the SideNav manages
114
+ its own internal scroll — let it fill the sidebar height exactly so the
115
+ footer stays pinned to the bottom rather than scrolling off screen. */
116
+ .a1-page-layout--viewport-height .a1-page-layout__sidebar:has(.a1-side-nav) {
117
+ overflow-y: hidden;
118
+ }
119
+
120
+ .a1-page-layout--viewport-height .a1-page-layout__sidebar .a1-side-nav {
121
+ position: relative;
122
+ top: auto;
123
+ height: 100%;
124
+ }
125
+
113
126
  .a1-page-layout--viewport-height .a1-page-layout__content {
114
127
  overflow: hidden;
115
128
  }
@@ -35,8 +35,6 @@
35
35
  font-size: var(--semantic-font-size-body-xs);
36
36
  font-weight: 600;
37
37
  color: var(--semantic-color-text-muted);
38
- text-transform: uppercase;
39
- letter-spacing: 0.08em;
40
38
  line-height: 1;
41
39
  }
42
40
 
@@ -21,11 +21,13 @@ export function Pagination({
21
21
  onChange,
22
22
  siblings = 1,
23
23
  size = "md",
24
+ className = "",
25
+ ...rest
24
26
  }) {
25
27
  const items = getPageItems(page, totalPages, siblings);
26
28
 
27
29
  return (
28
- <nav aria-label="Pagination" className={`a1-pagination a1-pagination--${size}`}>
30
+ <nav aria-label="Pagination" className={`a1-pagination a1-pagination--${size}${className ? ` ${className}` : ""}`} {...rest}>
29
31
  <IconButton
30
32
  icon="chevron_left"
31
33
  label="Previous page"
@@ -65,7 +65,7 @@ export function RadioGroup({
65
65
  <span className="a1-radio-group__legend-inner">
66
66
  {label}
67
67
  {required && resolvedSize === "comfortable" ? (
68
- <MessageBadge status="info" subtle>{requiredText}</MessageBadge>
68
+ <MessageBadge status="info" subtle size="sm" icon={null}>{requiredText}</MessageBadge>
69
69
  ) : required ? (
70
70
  <span className="a1-field__asterisk" aria-hidden="true"> *</span>
71
71
  ) : null}
@@ -15,7 +15,7 @@
15
15
  --a1-rb-input-nudge: var(--component-radio-group-input-nudge); /* top margin aligning circle center with label cap-height */
16
16
  --a1-rb-row-py: var(--component-radio-group-row-padding-block); /* vertical padding on each item row */
17
17
  --a1-rb-row-px: var(--component-radio-group-row-padding-inline); /* horizontal padding on each item row */
18
- --a1-rb-legend-size: var(--semantic-font-size-body-sm);
18
+ --a1-rb-legend-size: var(--semantic-font-size-form-label-default);
19
19
  --a1-rb-label-size: var(--semantic-font-size-body-md);
20
20
  --a1-rb-hint-size: var(--semantic-font-size-body-xs);
21
21
  --a1-rb-msg-size: var(--semantic-font-size-body-xs);
@@ -36,7 +36,7 @@
36
36
  --a1-rb-input-nudge: var(--component-radio-group-comfortable-input-nudge);
37
37
  --a1-rb-row-py: var(--component-radio-group-comfortable-row-padding-block);
38
38
  --a1-rb-row-px: var(--component-radio-group-comfortable-row-padding-inline);
39
- --a1-rb-legend-size: var(--semantic-font-size-body-md);
39
+ --a1-rb-legend-size: var(--semantic-font-size-form-label-comfortable);
40
40
  --a1-rb-label-size: var(--semantic-font-size-body-md);
41
41
  --a1-rb-hint-size: var(--semantic-font-size-body-sm);
42
42
  --a1-rb-msg-size: var(--semantic-font-size-body-sm);
@@ -64,7 +64,7 @@
64
64
  --a1-rb-input-nudge: var(--component-radio-group-compact-input-nudge);
65
65
  --a1-rb-row-py: var(--component-radio-group-compact-row-padding-block);
66
66
  --a1-rb-row-px: var(--component-radio-group-compact-row-padding-inline);
67
- --a1-rb-legend-size: var(--semantic-font-size-body-xs);
67
+ --a1-rb-legend-size: var(--semantic-font-size-form-label-compact);
68
68
  --a1-rb-label-size: var(--semantic-font-size-body-sm);
69
69
  --a1-rb-hint-size: var(--semantic-font-size-body-xs);
70
70
  --a1-rb-msg-size: var(--semantic-font-size-body-xs);
@@ -27,6 +27,14 @@ export interface SectionProps extends React.HTMLAttributes<HTMLElement> {
27
27
  height?: "screen" | "hero";
28
28
  /** Horizontal layout alignment for direct children. Responsive object syntax supported. */
29
29
  align?: ResponsiveAlignment;
30
+ /** Border thickness. Uses the same size tokens as Divider. Omit for no border. */
31
+ borderSize?: "xs" | "sm" | "md" | "lg";
32
+ /** Border pattern. Uses the same line styles as Divider. Default: "solid" */
33
+ borderStyle?: "solid" | "dashed" | "dotted";
34
+ /** Border color tone. Uses the same variants as Divider. Default: "subtle" */
35
+ borderVariant?: "subtle" | "strong" | "accent";
36
+ /** Border radius scale. */
37
+ radius?: "none" | "sm" | "md" | "lg" | "xl";
30
38
  children?: React.ReactNode;
31
39
  }
32
40
 
@@ -20,6 +20,10 @@ const VALID_GRADIENT_POSITIONS = [
20
20
  const VALID_CONTENT_WIDTHS = ["xs", "sm", "md", "lg", "xl", "2xl"];
21
21
  const VALID_HEIGHTS = ["screen", "hero"];
22
22
  const VALID_ALIGNMENTS = ["left", "center", "right"];
23
+ const VALID_BORDER_SIZES = ["xs", "sm", "md", "lg"];
24
+ const VALID_BORDER_STYLES = ["solid", "dashed", "dotted"];
25
+ const VALID_BORDER_VARIANTS = ["subtle", "strong", "accent"];
26
+ const VALID_RADII = ["none", "sm", "md", "lg", "xl"];
23
27
 
24
28
  export function Section({
25
29
  as: Component = "section",
@@ -32,6 +36,10 @@ export function Section({
32
36
  contentWidth,
33
37
  height,
34
38
  align,
39
+ borderSize,
40
+ borderStyle = "solid",
41
+ borderVariant = "subtle",
42
+ radius,
35
43
  className = "",
36
44
  children,
37
45
  ...props
@@ -88,6 +96,22 @@ export function Section({
88
96
  classes.push("a1-inverse");
89
97
  }
90
98
 
99
+ if (borderSize && VALID_BORDER_SIZES.includes(borderSize)) {
100
+ classes.push(`a1-section--border-${borderSize}`);
101
+ }
102
+
103
+ if (borderStyle && VALID_BORDER_STYLES.includes(borderStyle)) {
104
+ classes.push(`a1-section--border-${borderStyle}`);
105
+ }
106
+
107
+ if (borderVariant && VALID_BORDER_VARIANTS.includes(borderVariant)) {
108
+ classes.push(`a1-section--border-${borderVariant}`);
109
+ }
110
+
111
+ if (radius && VALID_RADII.includes(radius)) {
112
+ classes.push(`a1-section--radius-${radius}`);
113
+ }
114
+
91
115
  if (className) classes.push(className);
92
116
 
93
117
  const innerClasses = [
@@ -10,6 +10,10 @@
10
10
  --a1-section-gradient-anchor: center;
11
11
  --a1-section-gradient-strength: var(--component-section-gradient-strength);
12
12
  --a1-section-justify-items: stretch;
13
+ --a1-section-border-size: 0;
14
+ --a1-section-border-style: solid;
15
+ --a1-section-border-color: transparent;
16
+ border: var(--a1-section-border-size) var(--a1-section-border-style) var(--a1-section-border-color);
13
17
  }
14
18
 
15
19
  .a1-section.a1-inverse {
@@ -23,6 +27,29 @@
23
27
  .a1-section--surface-panel { --a1-section-surface: var(--semantic-color-surface-panel); background: var(--a1-section-surface); }
24
28
  .a1-section--surface-raised { --a1-section-surface: var(--semantic-color-surface-raised); background: var(--a1-section-surface); }
25
29
 
30
+ /* ── Border ────────────────────────────────────────────────────────────────── */
31
+
32
+ .a1-section--border-xs { --a1-section-border-size: var(--component-divider-size-xs); }
33
+ .a1-section--border-sm { --a1-section-border-size: var(--component-divider-size-sm); }
34
+ .a1-section--border-md { --a1-section-border-size: var(--component-divider-size-md); }
35
+ .a1-section--border-lg { --a1-section-border-size: var(--component-divider-size-lg); }
36
+
37
+ .a1-section--border-solid { --a1-section-border-style: solid; }
38
+ .a1-section--border-dashed { --a1-section-border-style: dashed; }
39
+ .a1-section--border-dotted { --a1-section-border-style: dotted; }
40
+
41
+ .a1-section--border-subtle { --a1-section-border-color: var(--semantic-color-border-subtle); }
42
+ .a1-section--border-strong { --a1-section-border-color: var(--semantic-color-border-strong); }
43
+ .a1-section--border-accent { --a1-section-border-color: var(--semantic-color-text-accent); }
44
+
45
+ /* ── Radius ────────────────────────────────────────────────────────────────── */
46
+
47
+ .a1-section--radius-none { border-radius: 0; }
48
+ .a1-section--radius-sm { border-radius: var(--base-radius-sm); }
49
+ .a1-section--radius-md { border-radius: var(--base-radius-md); }
50
+ .a1-section--radius-lg { border-radius: var(--base-radius-lg); }
51
+ .a1-section--radius-xl { border-radius: var(--base-radius-xl); }
52
+
26
53
  /* ── Gradient wash ─────────────────────────────────────────────────────────── */
27
54
 
28
55
  .a1-section--gradient-accent { --a1-section-gradient-color: var(--semantic-color-action-background); }
@@ -84,6 +111,7 @@
84
111
 
85
112
  .a1-section--height-screen {
86
113
  min-height: 100svh;
114
+ align-content: start;
87
115
  }
88
116
 
89
117
  /* Fills the viewport minus the sticky top header — use for hero/landing sections. */
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ export interface SnackbarProps {
4
+ /** Controls visibility — renders nothing when false. Default: false */
5
+ open?: boolean;
6
+ /** Message content displayed inside the snackbar. */
7
+ children?: React.ReactNode;
8
+ /** Label for the optional action button. Both `actionLabel` and `onAction` must be provided to show the button. */
9
+ actionLabel?: string;
10
+ /** Called when the action button is clicked. Both `actionLabel` and `onAction` must be provided to show the button. */
11
+ onAction?: () => void;
12
+ /** Called when the dismiss icon button is clicked. Omit to hide the dismiss button. */
13
+ onClose?: () => void;
14
+ /**
15
+ * Snackbar position.
16
+ * Default: "bottom"
17
+ */
18
+ position?: 'bottom' | 'bottom-left' | 'bottom-right' | 'top' | 'top-left' | 'top-right';
19
+ /** ARIA role. Default: "status" (aria-live="polite"). */
20
+ role?: string;
21
+ className?: string;
22
+ }
23
+
24
+ export declare function Snackbar(props: SnackbarProps): React.ReactElement | null;
@@ -2,7 +2,6 @@ import "./snackbar.css";
2
2
  import { Button } from "../button/Button.jsx";
3
3
  import { IconButton } from "../icon-button/IconButton.jsx";
4
4
 
5
- const variants = ["default", "success", "info", "warn", "error"];
6
5
  const positions = ["bottom", "bottom-left", "bottom-right", "top", "top-left", "top-right"];
7
6
 
8
7
  export function Snackbar({
@@ -11,21 +10,25 @@ export function Snackbar({
11
10
  actionLabel,
12
11
  onAction,
13
12
  onClose,
14
- variant = "default",
13
+ variant: ignoredVariant,
15
14
  position = "bottom",
16
- inverse = true,
15
+ inverse: ignoredInverse,
17
16
  role,
18
17
  className = "",
19
18
  ...props
20
19
  }) {
21
20
  if (!open) return null;
22
21
 
23
- const resolvedVariant = variants.includes(variant) ? variant : "default";
22
+ // Kept out of the DOM for older call sites; Snackbar now has one visual style.
23
+ void ignoredVariant;
24
+ // Kept out of the DOM for older call sites; inverse is now internal.
25
+ void ignoredInverse;
26
+
24
27
  const resolvedPosition = positions.includes(position) ? position : "bottom";
25
28
  const classes = [
26
29
  "a1-snackbar",
27
- inverse && "a1-inverse",
28
- `a1-snackbar--${resolvedVariant}`,
30
+ "a1-inverse",
31
+ "a1-snackbar--default",
29
32
  `a1-snackbar--${resolvedPosition}`,
30
33
  className,
31
34
  ].filter(Boolean).join(" ");
@@ -33,8 +36,8 @@ export function Snackbar({
33
36
  return (
34
37
  <div
35
38
  className={classes}
36
- role={role ?? (resolvedVariant === "error" ? "alert" : "status")}
37
- aria-live={resolvedVariant === "error" ? "assertive" : "polite"}
39
+ role={role ?? "status"}
40
+ aria-live="polite"
38
41
  {...props}
39
42
  >
40
43
  <div className="a1-snackbar__content">{children}</div>
@@ -66,29 +66,14 @@
66
66
  flex: 1;
67
67
  }
68
68
 
69
- .a1-snackbar--default,
70
- .a1-snackbar--info {
71
- --a1-snackbar-background: var(--semantic-color-surface-inverse, var(--base-color-neutral-900));
72
- --a1-snackbar-border: var(--semantic-color-surface-inverse, var(--base-color-neutral-900));
73
- --a1-snackbar-foreground: var(--semantic-color-text-default, var(--base-color-neutral-0));
74
- }
75
-
76
- .a1-snackbar--success {
77
- --a1-snackbar-background: var(--semantic-color-status-success-background);
78
- --a1-snackbar-border: var(--semantic-color-status-success-border, var(--semantic-color-status-success-background));
79
- --a1-snackbar-foreground: var(--semantic-color-status-success-foreground);
80
- }
69
+ .a1-snackbar.a1-snackbar--default {
70
+ --a1-snackbar-background: var(--base-color-neutral-900);
71
+ --a1-snackbar-border: var(--base-color-neutral-900);
72
+ --a1-snackbar-foreground: var(--base-color-neutral-0);
81
73
 
82
- .a1-snackbar--warn {
83
- --a1-snackbar-background: var(--semantic-color-status-warn-background);
84
- --a1-snackbar-border: var(--semantic-color-status-warn-border, var(--semantic-color-status-warn-background));
85
- --a1-snackbar-foreground: var(--semantic-color-status-warn-foreground);
86
- }
87
-
88
- .a1-snackbar--error {
89
- --a1-snackbar-background: var(--semantic-color-status-error-background);
90
- --a1-snackbar-border: var(--semantic-color-status-error-border, var(--semantic-color-status-error-background));
91
- --a1-snackbar-foreground: var(--semantic-color-status-error-foreground);
74
+ background: var(--a1-snackbar-background);
75
+ border-color: var(--a1-snackbar-border);
76
+ color: var(--a1-snackbar-foreground);
92
77
  }
93
78
 
94
79
  @media (max-width: 720px) {
@@ -74,6 +74,7 @@ export function Stack({
74
74
  justify = "start",
75
75
  wrap = false,
76
76
  className = "",
77
+ style: styleProp,
77
78
  children,
78
79
  ...props
79
80
  }) {
@@ -89,7 +90,7 @@ export function Stack({
89
90
  "--a1-stack-wrap": wrap ? "wrap" : "nowrap",
90
91
  ...getResponsiveDirectionStyle(direction),
91
92
  ...getResponsiveJustifyStyle(justify),
92
- ...props.style,
93
+ ...styleProp,
93
94
  };
94
95
 
95
96
  return (
@@ -11,4 +11,11 @@ export interface StickyActionsProps extends Omit<HTMLAttributes<HTMLDivElement>,
11
11
  children?: ReactNode;
12
12
  }
13
13
 
14
+ /**
15
+ * Fixed bottom action bar for flows, wizards, and multi-step forms.
16
+ *
17
+ * Renders a position:fixed bar at the bottom of the viewport and an invisible
18
+ * spacer sibling in document flow. The spacer height is measured via ResizeObserver
19
+ * and kept in sync automatically — no manual bottom padding needed on the page.
20
+ */
14
21
  export declare function StickyActions(props: StickyActionsProps): JSX.Element;
@@ -1,9 +1,21 @@
1
+ import { useEffect, useRef, useState } from "react";
1
2
  import "./sticky-actions.css";
2
3
 
3
4
  const VALID_CONTENT_WIDTHS = ["xs", "sm", "md", "lg", "xl", "2xl"];
4
5
 
5
6
  export function StickyActions({ contentWidth, className = "", children, ...props }) {
6
7
  const resolvedWidth = VALID_CONTENT_WIDTHS.includes(contentWidth) ? contentWidth : null;
8
+ const barRef = useRef(null);
9
+ const [offset, setOffset] = useState(0);
10
+
11
+ useEffect(() => {
12
+ if (!barRef.current) return;
13
+ const ro = new ResizeObserver(([entry]) => {
14
+ setOffset(entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height);
15
+ });
16
+ ro.observe(barRef.current);
17
+ return () => ro.disconnect();
18
+ }, []);
7
19
 
8
20
  const innerClass = [
9
21
  "a1-sticky-actions__inner",
@@ -11,10 +23,17 @@ export function StickyActions({ contentWidth, className = "", children, ...props
11
23
  ].filter(Boolean).join(" ");
12
24
 
13
25
  return (
14
- <div className={["a1-sticky-actions", className].filter(Boolean).join(" ")} {...props}>
15
- <div className={innerClass}>
16
- {children}
26
+ <>
27
+ {/* Spacer holds the bar's height in document flow so content above is
28
+ never hidden. Height is measured and kept in sync via ResizeObserver. */}
29
+ <div style={{ height: offset }} aria-hidden="true" />
30
+ <div
31
+ ref={barRef}
32
+ className={["a1-sticky-actions", className].filter(Boolean).join(" ")}
33
+ {...props}
34
+ >
35
+ <div className={innerClass}>{children}</div>
17
36
  </div>
18
- </div>
37
+ </>
19
38
  );
20
39
  }
@@ -5,9 +5,6 @@
5
5
  z-index: var(--component-sticky-actions-z-index);
6
6
  background: var(--component-sticky-actions-background);
7
7
  border-block-start: var(--component-sticky-actions-border-width) solid var(--semantic-color-border-subtle);
8
- padding-block-start: var(--component-sticky-actions-padding-block);
9
- padding-block-end: calc(var(--component-sticky-actions-padding-block) + env(safe-area-inset-bottom, 0px));
10
- padding-inline: var(--component-sticky-actions-padding-inline);
11
8
  box-sizing: border-box;
12
9
  }
13
10
 
@@ -15,6 +12,11 @@
15
12
  display: flex;
16
13
  flex-direction: column;
17
14
  gap: var(--component-sticky-actions-gap);
15
+ /* Padding lives here so max-width + margin-inline: auto center correctly
16
+ against the full viewport width, not the outer padded content area. */
17
+ padding-inline: var(--component-sticky-actions-padding-inline);
18
+ padding-block-start: var(--component-sticky-actions-padding-block);
19
+ padding-block-end: calc(var(--component-sticky-actions-padding-block) + env(safe-area-inset-bottom, 0px));
18
20
  width: 100%;
19
21
  margin-inline: auto;
20
22
  box-sizing: border-box;
@@ -19,6 +19,8 @@ export interface TabsProps {
19
19
  * Default: 1
20
20
  */
21
21
  level?: 1 | 2;
22
+ /** Size variant. Default: undefined (standard) */
23
+ size?: "compact";
22
24
  className?: string;
23
25
  children?: React.ReactNode;
24
26
  }
@@ -6,11 +6,11 @@ const TabsContext = createContext(null);
6
6
 
7
7
  /* ─── Tabs ─────────────────────────────────────────────────────────────────── */
8
8
 
9
- export function Tabs({ children, value, onChange, variant = "line", level = 1, className = "" }) {
9
+ export function Tabs({ children, value, onChange, variant = "line", level = 1, size, className = "" }) {
10
10
  const uid = useId();
11
11
  return (
12
- <TabsContext.Provider value={{ value, onChange, variant, level, uid }}>
13
- <div className={["a1-tabs", `a1-tabs--level-${level}`, className].filter(Boolean).join(" ")}>
12
+ <TabsContext.Provider value={{ value, onChange, variant, level, size, uid }}>
13
+ <div className={["a1-tabs", `a1-tabs--level-${level}`, size && `a1-tabs--${size}`, className].filter(Boolean).join(" ")}>
14
14
  {children}
15
15
  </div>
16
16
  </TabsContext.Provider>