@gtivr4/a1-design-system-react 0.12.1 → 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 (62) 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 -7
  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/tabs/Tabs.d.ts +2 -0
  55. package/src/components/tabs/Tabs.jsx +3 -3
  56. package/src/components/tabs/tabs.css +95 -0
  57. package/src/components/top-header/TopHeader.jsx +2 -0
  58. package/src/components/tree-menu/TreeMenu.d.ts +54 -0
  59. package/src/components/tree-menu/TreeMenu.jsx +500 -0
  60. package/src/components/tree-menu/tree-menu.css +254 -0
  61. package/src/index.js +2 -0
  62. 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 (
@@ -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>
@@ -225,6 +225,77 @@
225
225
  border-bottom-left-radius: var(--base-radius-lg);
226
226
  }
227
227
 
228
+ /* ── Pills variant ──────────────────────────────────────────────────────── */
229
+
230
+ .a1-tab-list-wrapper--pills {
231
+ align-items: center;
232
+ }
233
+
234
+ .a1-tab-list--pills {
235
+ gap: var(--base-spacing-8);
236
+ flex-wrap: wrap;
237
+ }
238
+
239
+ .a1-tab--pills {
240
+ padding: var(--component-tab-padding-block) var(--component-tab-padding-inline);
241
+ font-size: var(--semantic-font-size-body-sm);
242
+ font-weight: var(--base-font-weight-medium);
243
+ color: var(--semantic-color-text-muted);
244
+ background: var(--semantic-color-surface-raised);
245
+ border-radius: var(--base-radius-pill);
246
+ }
247
+
248
+ .a1-tab--pills:hover:not([aria-selected="true"]) {
249
+ color: var(--semantic-color-text-default);
250
+ background: var(--semantic-color-surface-panel);
251
+ }
252
+
253
+ .a1-tab--pills[aria-selected="true"] {
254
+ color: var(--semantic-color-action-foreground);
255
+ background: var(--semantic-color-action-background);
256
+ cursor: default;
257
+ }
258
+
259
+ .a1-tab--pills[aria-selected="true"] .a1-tab__count {
260
+ background: color-mix(in srgb, var(--semantic-color-action-foreground) 24%, transparent);
261
+ color: var(--semantic-color-action-foreground);
262
+ }
263
+
264
+ /* ── Segment variant (mirrors SegmentedControl) ─────────────────────────── */
265
+
266
+ .a1-tab-list-wrapper--segment {
267
+ align-items: center;
268
+ }
269
+
270
+ .a1-tab-list--segment {
271
+ gap: var(--component-segmented-gap);
272
+ padding: var(--component-segmented-padding);
273
+ background: var(--semantic-color-surface-raised);
274
+ border: var(--component-segmented-border-width) solid var(--semantic-color-border-default);
275
+ border-radius: var(--base-radius-control);
276
+ }
277
+
278
+ .a1-tab--segment {
279
+ padding: var(--component-segmented-segment-padding-block) var(--component-segmented-segment-padding-inline);
280
+ font-size: var(--semantic-font-size-body-sm);
281
+ font-weight: var(--base-font-weight-medium);
282
+ color: var(--semantic-color-text-muted);
283
+ border-radius: calc(var(--base-radius-control) - var(--component-segmented-padding));
284
+ justify-content: center;
285
+ }
286
+
287
+ .a1-tab--segment:hover:not([aria-selected="true"]) {
288
+ color: var(--semantic-color-text-default);
289
+ background: var(--semantic-color-surface-panel);
290
+ }
291
+
292
+ .a1-tab--segment[aria-selected="true"] {
293
+ color: var(--semantic-color-text-default);
294
+ background: var(--semantic-color-surface-page);
295
+ box-shadow: var(--semantic-shadow-xs);
296
+ cursor: default;
297
+ }
298
+
228
299
  /* ── Progress variant ───────────────────────────────────────────────────── */
229
300
 
230
301
  .a1-tab-list-wrapper--progress {
@@ -470,3 +541,27 @@
470
541
  .a1-tab-panel--progress {
471
542
  padding: var(--base-spacing-24) 0;
472
543
  }
544
+
545
+ /* ─── Compact size ──────────────────────────────────────────────────────────── */
546
+
547
+ .a1-tabs--compact .a1-tab {
548
+ padding: var(--base-spacing-6) var(--base-spacing-8);
549
+ font-size: var(--semantic-font-size-body-sm);
550
+ gap: var(--base-spacing-4);
551
+ }
552
+
553
+ .a1-tabs--compact .a1-tab--line {
554
+ padding: var(--base-spacing-6) var(--base-spacing-8);
555
+ }
556
+
557
+ .a1-tabs--compact .a1-tab--folder {
558
+ padding: var(--base-spacing-6) var(--base-spacing-8);
559
+ }
560
+
561
+ .a1-tabs--compact .a1-tab--pills {
562
+ padding: var(--base-spacing-4) var(--base-spacing-12);
563
+ }
564
+
565
+ .a1-tabs--compact .a1-tab--segment {
566
+ padding: var(--component-segmented-segment-padding-block-sm) var(--component-segmented-segment-padding-inline-sm);
567
+ }
@@ -588,6 +588,7 @@ export function TopHeader({
588
588
  loginButton,
589
589
  navIconPosition = "start",
590
590
  className = "",
591
+ ...rest
591
592
  }) {
592
593
  const [navMode, setNavMode] = useState(() => resolveNavMode(navIconPosition));
593
594
  const [openSubmenu, setOpenSubmenu] = useState(null);
@@ -641,6 +642,7 @@ export function TopHeader({
641
642
  navMode === "hidden" && "a1-top-header--nav-hidden",
642
643
  className,
643
644
  ].filter(Boolean).join(" ")}
645
+ {...rest}
644
646
  >
645
647
  <button
646
648
  type="button"