@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
@@ -0,0 +1,146 @@
1
+ import { createPortal } from 'react-dom';
2
+ import { useEffect, useRef } from 'react';
3
+ import { Icon } from '../icon/Icon.jsx';
4
+ import './context-menu.css';
5
+
6
+ const FOCUSABLE = 'button:not([disabled]):not([aria-disabled="true"])';
7
+
8
+ // ── ContextMenu ───────────────────────────────────────────────────────────────
9
+
10
+ /**
11
+ * Controlled context menu portaled to document.body.
12
+ *
13
+ * Usage:
14
+ * const [menu, setMenu] = useState(null);
15
+ * <div onContextMenu={e => { e.preventDefault(); setMenu({ x: e.clientX, y: e.clientY }); }}>
16
+ * <ContextMenu open={!!menu} x={menu?.x ?? 0} y={menu?.y ?? 0}
17
+ * items={[...]} onClose={() => setMenu(null)} />
18
+ * </div>
19
+ */
20
+ export function ContextMenu({
21
+ open = false,
22
+ x = 0,
23
+ y = 0,
24
+ items = [],
25
+ onClose,
26
+ 'aria-label': ariaLabel = 'Context menu',
27
+ }) {
28
+ const ref = useRef(null);
29
+
30
+ useEffect(() => {
31
+ if (!open) return;
32
+
33
+ // Focus the first item when the menu opens
34
+ requestAnimationFrame(() => {
35
+ ref.current?.querySelector(FOCUSABLE)?.focus();
36
+ });
37
+
38
+ function handleMouseDown(e) {
39
+ if (ref.current && !ref.current.contains(e.target)) onClose?.();
40
+ }
41
+
42
+ function handleKeyDown(e) {
43
+ if (!ref.current) return;
44
+ const focusable = Array.from(ref.current.querySelectorAll(FOCUSABLE));
45
+ const current = focusable.indexOf(document.activeElement);
46
+
47
+ switch (e.key) {
48
+ case 'Escape':
49
+ e.preventDefault();
50
+ onClose?.();
51
+ break;
52
+ case 'ArrowDown': {
53
+ e.preventDefault();
54
+ const next = current < focusable.length - 1 ? current + 1 : 0;
55
+ focusable[next]?.focus();
56
+ break;
57
+ }
58
+ case 'ArrowUp': {
59
+ e.preventDefault();
60
+ const prev = current > 0 ? current - 1 : focusable.length - 1;
61
+ focusable[prev]?.focus();
62
+ break;
63
+ }
64
+ case 'Home':
65
+ e.preventDefault();
66
+ focusable[0]?.focus();
67
+ break;
68
+ case 'End':
69
+ e.preventDefault();
70
+ focusable[focusable.length - 1]?.focus();
71
+ break;
72
+ }
73
+ }
74
+
75
+ document.addEventListener('mousedown', handleMouseDown);
76
+ document.addEventListener('keydown', handleKeyDown);
77
+ return () => {
78
+ document.removeEventListener('mousedown', handleMouseDown);
79
+ document.removeEventListener('keydown', handleKeyDown);
80
+ };
81
+ }, [open, onClose]);
82
+
83
+ if (!open) return null;
84
+
85
+ // Clamp to viewport edges (16px safe margin)
86
+ const margin = 16;
87
+ const menuWidth = 240;
88
+ const menuHeight = items.length * 36 + 32;
89
+ const adjustedX = Math.min(x, window.innerWidth - menuWidth - margin);
90
+ const adjustedY = Math.min(y + 4, window.innerHeight - menuHeight - margin);
91
+
92
+ return createPortal(
93
+ <div
94
+ ref={ref}
95
+ className="a1-context-menu"
96
+ style={{ left: Math.max(margin, adjustedX), top: Math.max(margin, adjustedY) }}
97
+ role="menu"
98
+ aria-label={ariaLabel}
99
+ >
100
+ {items.map((entry) => {
101
+ if (entry.type === 'divider') {
102
+ return <hr key={entry.id} className="a1-context-menu__divider" aria-hidden="true" />;
103
+ }
104
+
105
+ if (entry.type === 'group') {
106
+ return (
107
+ <span key={entry.id} className="a1-context-menu__heading" role="presentation">
108
+ {entry.label}
109
+ </span>
110
+ );
111
+ }
112
+
113
+ // Default: 'item'
114
+ const isDestructive = entry.variant === 'destructive';
115
+ const isActive = !!entry.active;
116
+
117
+ return (
118
+ <button
119
+ key={entry.id}
120
+ type="button"
121
+ role="menuitem"
122
+ className={[
123
+ 'a1-context-menu__item',
124
+ isDestructive && 'a1-context-menu__item--destructive',
125
+ isActive && 'a1-context-menu__item--active',
126
+ ].filter(Boolean).join(' ')}
127
+ disabled={entry.disabled}
128
+ onClick={() => {
129
+ entry.onClick?.();
130
+ onClose?.();
131
+ }}
132
+ >
133
+ {entry.icon && (
134
+ <Icon name={entry.icon} className="a1-context-menu__icon" aria-hidden="true" />
135
+ )}
136
+ <span className="a1-context-menu__item-label">{entry.label}</span>
137
+ {entry.shortcut && (
138
+ <kbd className="a1-context-menu__kbd">{entry.shortcut}</kbd>
139
+ )}
140
+ </button>
141
+ );
142
+ })}
143
+ </div>,
144
+ document.body,
145
+ );
146
+ }
@@ -0,0 +1,107 @@
1
+ /* ── ContextMenu ── */
2
+
3
+ .a1-context-menu {
4
+ position: fixed;
5
+ z-index: 1000;
6
+ background: var(--semantic-color-surface-page);
7
+ border: 1px solid var(--semantic-color-border-subtle);
8
+ border-radius: var(--base-radius-md);
9
+ box-shadow: var(--semantic-shadow-md);
10
+ padding: var(--base-spacing-4);
11
+ min-inline-size: 160px;
12
+ max-inline-size: 240px;
13
+ }
14
+
15
+ .a1-context-menu__heading {
16
+ display: block;
17
+ padding: var(--base-spacing-4) var(--base-spacing-8);
18
+ font-family: var(--component-paragraph-font-family, inherit);
19
+ font-size: var(--semantic-font-size-body-xs);
20
+ color: var(--semantic-color-text-muted);
21
+ user-select: none;
22
+ }
23
+
24
+ .a1-context-menu__divider {
25
+ margin-block: var(--base-spacing-4);
26
+ border: none;
27
+ border-block-start: 1px solid var(--semantic-color-border-subtle);
28
+ }
29
+
30
+ .a1-context-menu__item {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: var(--base-spacing-8);
34
+ inline-size: 100%;
35
+ padding-block: var(--base-spacing-6);
36
+ padding-inline: var(--base-spacing-8);
37
+ border: none;
38
+ border-radius: var(--base-radius-sm);
39
+ background: transparent;
40
+ font-family: var(--component-paragraph-font-family, inherit);
41
+ font-size: var(--semantic-font-size-body-sm);
42
+ color: var(--semantic-color-text-default);
43
+ cursor: pointer;
44
+ text-align: start;
45
+ transition: background var(--semantic-motion-duration-fast, 120ms) ease;
46
+ }
47
+
48
+ .a1-context-menu__item:hover {
49
+ background: color-mix(in srgb, transparent, var(--semantic-color-text-default) 6%);
50
+ }
51
+
52
+ .a1-context-menu__item:focus-visible {
53
+ outline: 2px solid var(--semantic-color-text-accent);
54
+ outline-offset: -1px;
55
+ }
56
+
57
+ .a1-context-menu__item[disabled],
58
+ .a1-context-menu__item[aria-disabled='true'] {
59
+ opacity: 0.4;
60
+ cursor: not-allowed;
61
+ pointer-events: none;
62
+ }
63
+
64
+ .a1-context-menu__item--active {
65
+ background: var(--semantic-color-action-background);
66
+ color: var(--semantic-color-action-foreground);
67
+ }
68
+
69
+ .a1-context-menu__item--active:hover {
70
+ background: var(--semantic-color-action-background-hover, var(--semantic-color-action-background));
71
+ }
72
+
73
+ .a1-context-menu__item--destructive {
74
+ color: var(--semantic-color-status-error-text);
75
+ }
76
+
77
+ .a1-context-menu__item--destructive:hover {
78
+ background: var(--semantic-color-status-error-surface);
79
+ }
80
+
81
+ .a1-context-menu__icon {
82
+ flex-shrink: 0;
83
+ font-size: 16px !important;
84
+ line-height: 1 !important;
85
+ }
86
+
87
+ .a1-context-menu__item-label {
88
+ flex: 1 1 auto;
89
+ min-inline-size: 0;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ white-space: nowrap;
93
+ }
94
+
95
+ .a1-context-menu__kbd {
96
+ flex-shrink: 0;
97
+ margin-inline-start: auto;
98
+ font-family: var(--base-font-family-mono, monospace);
99
+ font-size: var(--semantic-font-size-body-xs);
100
+ line-height: 1;
101
+ color: var(--semantic-color-text-muted);
102
+ background: color-mix(in srgb, transparent, var(--semantic-color-text-default) 6%);
103
+ border: 1px solid var(--semantic-color-border-subtle);
104
+ border-radius: var(--base-radius-xs, 2px);
105
+ padding-inline: var(--base-spacing-4);
106
+ padding-block: var(--base-spacing-2);
107
+ }
@@ -556,7 +556,7 @@ export function DataTable({
556
556
  value={activeFilterValue}
557
557
  onChange={updateFilterValue}
558
558
  searchValue={activeSearchValue}
559
- onSearchChange={updateSearchValue}
559
+ onSearchChange={onSearchChange != null || searchValue !== undefined || searchableColumns?.length > 0 ? updateSearchValue : undefined}
560
560
  searchColumn={activeSearchColumn}
561
561
  onSearchColumnChange={updateSearchColumn}
562
562
  searchableColumns={searchableColumns}
@@ -68,6 +68,21 @@
68
68
  gap: var(--a1-definition-list-row-gap);
69
69
  }
70
70
 
71
+ .a1-definition-list--column.a1-definition-list--sm {
72
+ --a1-definition-list-gap: var(--component-definition-list-gap-md);
73
+ --a1-definition-list-row-gap: var(--base-spacing-4);
74
+ }
75
+
76
+ .a1-definition-list--column.a1-definition-list--md {
77
+ --a1-definition-list-gap: var(--component-definition-list-gap-lg);
78
+ --a1-definition-list-row-gap: var(--component-definition-list-row-gap-sm);
79
+ }
80
+
81
+ .a1-definition-list--column.a1-definition-list--lg {
82
+ --a1-definition-list-gap: var(--base-spacing-20);
83
+ --a1-definition-list-row-gap: var(--component-definition-list-row-gap-md);
84
+ }
85
+
71
86
  .a1-definition-list--row {
72
87
  display: flex;
73
88
  flex-direction: column;
@@ -9,8 +9,10 @@ export interface DividerProps extends React.HTMLAttributes<HTMLHRElement> {
9
9
  * @example orientation={{ xs: "horizontal", md: "vertical" }}
10
10
  */
11
11
  orientation?: Orientation | Partial<Record<Breakpoints, Orientation>>;
12
- /** Visual style. Default: "subtle" */
13
- variant?: "subtle" | "strong" | "accent" | "dashed" | "dotted";
12
+ /** Color tone. Default: "subtle" */
13
+ variant?: "subtle" | "strong" | "accent";
14
+ /** Border pattern. Default: "solid" */
15
+ lineStyle?: "solid" | "dashed" | "dotted";
14
16
  /** Line thickness. Default: "xs" */
15
17
  size?: "xs" | "sm" | "md" | "lg";
16
18
  /** Block-axis margin (space above and below for horizontal, left/right for vertical). Default: "sm" */
@@ -1,7 +1,8 @@
1
1
  import "./divider.css";
2
2
 
3
3
  const orientations = ["horizontal", "vertical"];
4
- const variants = ["subtle", "strong", "accent", "dashed", "dotted"];
4
+ const variants = ["subtle", "strong", "accent"];
5
+ const lineStyles = ["solid", "dashed", "dotted"];
5
6
  const sizes = ["xs", "sm", "md", "lg"];
6
7
  const spacing = ["none", "xs", "sm", "md", "lg", "xl", "xxl"];
7
8
  const breakpoints = ["xs", "sm", "md", "lg", "xl"];
@@ -33,13 +34,16 @@ function getOrientationClasses(orientation) {
33
34
  export function Divider({
34
35
  orientation = "horizontal",
35
36
  variant = "subtle",
37
+ lineStyle = "solid",
36
38
  size = "xs",
37
39
  space = "sm",
38
40
  decorative = true,
39
41
  className = "",
40
42
  ...props
41
43
  }) {
44
+ const legacyLineStyle = lineStyles.includes(variant) ? variant : undefined;
42
45
  const resolvedVariant = variants.includes(variant) ? variant : "subtle";
46
+ const resolvedLineStyle = legacyLineStyle || (lineStyles.includes(lineStyle) ? lineStyle : "solid");
43
47
  const resolvedSize = sizes.includes(size) ? size : "xs";
44
48
  const resolvedSpace = spacing.includes(space) ? space : "sm";
45
49
  const resolvedOrientation = resolveBaseOrientation(orientation);
@@ -48,6 +52,7 @@ export function Divider({
48
52
  "a1-divider",
49
53
  ...getOrientationClasses(orientation),
50
54
  `a1-divider--${resolvedVariant}`,
55
+ `a1-divider--${resolvedLineStyle}`,
51
56
  `a1-divider--${resolvedSize}`,
52
57
  `a1-divider--space-${resolvedSpace}`,
53
58
  className,
@@ -151,11 +151,15 @@
151
151
 
152
152
  /* ── Variant ─────────────────────────────────────────────────────────────── */
153
153
 
154
- .a1-divider--subtle { --a1-divider-style: solid; color: var(--semantic-color-border-subtle); }
155
- .a1-divider--strong { --a1-divider-style: solid; color: var(--semantic-color-border-strong); }
156
- .a1-divider--accent { --a1-divider-style: solid; color: var(--semantic-color-text-accent); }
157
- .a1-divider--dashed { --a1-divider-style: dashed; color: var(--semantic-color-border-subtle); }
158
- .a1-divider--dotted { --a1-divider-style: dotted; color: var(--semantic-color-border-subtle); }
154
+ .a1-divider--subtle { color: var(--semantic-color-border-subtle); }
155
+ .a1-divider--strong { color: var(--semantic-color-border-strong); }
156
+ .a1-divider--accent { color: var(--semantic-color-text-accent); }
157
+
158
+ /* ── Line style ──────────────────────────────────────────────────────────── */
159
+
160
+ .a1-divider--solid { --a1-divider-style: solid; }
161
+ .a1-divider--dashed { --a1-divider-style: dashed; }
162
+ .a1-divider--dotted { --a1-divider-style: dotted; }
159
163
 
160
164
  /* ── Space ───────────────────────────────────────────────────────────────── */
161
165
  /* --a1-divider-space-value is routed to margin-block or margin-inline
@@ -1,10 +1,25 @@
1
+ import { useState } from "react";
1
2
  import { TextField } from "./TextField.jsx";
2
3
 
3
- export function DateField({ className = "", ...props }) {
4
+ export function DateField({ className = "", value, defaultValue, onChange, ...props }) {
5
+ const isControlled = value != null;
6
+ const [internal, setInternal] = useState(defaultValue ?? "");
7
+ const current = isControlled ? value : internal;
8
+ // Mute the native mm/dd/yyyy format placeholder while the field is empty.
9
+ const emptyClass = current ? "" : "a1-field--mask-empty";
10
+
11
+ function handleChange(event) {
12
+ if (!isControlled) setInternal(event.target.value);
13
+ onChange?.(event);
14
+ }
15
+
4
16
  return (
5
17
  <TextField
6
18
  type="date"
7
- className={`a1-field--fit ${className}`.trim()}
19
+ className={`a1-field--fit ${emptyClass} ${className}`.replace(/\s+/g, " ").trim()}
20
+ value={isControlled ? value : undefined}
21
+ defaultValue={isControlled ? undefined : defaultValue}
22
+ onChange={handleChange}
8
23
  {...props}
9
24
  />
10
25
  );
@@ -52,7 +52,7 @@ export const SelectField = forwardRef(function SelectField({
52
52
  <label className="a1-field__label" htmlFor={id}>
53
53
  {label}
54
54
  {required && resolvedSize === "comfortable" ? (
55
- <MessageBadge status="info" subtle>{requiredText}</MessageBadge>
55
+ <MessageBadge status="info" subtle size="sm" icon={null}>{requiredText}</MessageBadge>
56
56
  ) : required ? (
57
57
  <span className="a1-field__asterisk" aria-hidden="true"> *</span>
58
58
  ) : null}
@@ -14,6 +14,8 @@ export interface TextFieldProps extends Omit<React.InputHTMLAttributes<HTMLInput
14
14
  required?: boolean;
15
15
  disabled?: boolean;
16
16
  readOnly?: boolean;
17
+ /** Autofill hint forwarded to the native input, e.g. "email", "current-password", "tel", "postal-code", "cc-number", "off". Improves browser and password-manager autofill. */
18
+ autoComplete?: string;
17
19
  /** Element rendered inside the field control (e.g. a unit suffix) */
18
20
  inputOverlay?: React.ReactNode;
19
21
  }
@@ -53,7 +53,7 @@ export const TextField = forwardRef(function TextField({
53
53
  <label className="a1-field__label" htmlFor={id}>
54
54
  {label}
55
55
  {required && resolvedSize === "comfortable" ? (
56
- <MessageBadge status="info" subtle>{requiredText}</MessageBadge>
56
+ <MessageBadge status="info" subtle size="sm" icon={null}>{requiredText}</MessageBadge>
57
57
  ) : required ? (
58
58
  <span className="a1-field__asterisk" aria-hidden="true"> *</span>
59
59
  ) : null}
@@ -106,7 +106,7 @@ export const TextareaField = forwardRef(function TextareaField({
106
106
  <label className="a1-field__label" htmlFor={id}>
107
107
  {label}
108
108
  {required && resolvedSize === "comfortable" ? (
109
- <MessageBadge status="info" subtle>{requiredText}</MessageBadge>
109
+ <MessageBadge status="info" subtle size="sm" icon={null}>{requiredText}</MessageBadge>
110
110
  ) : required ? (
111
111
  <span className="a1-field__asterisk" aria-hidden="true"> *</span>
112
112
  ) : null}
@@ -1,10 +1,25 @@
1
+ import { useState } from "react";
1
2
  import { TextField } from "./TextField.jsx";
2
3
 
3
- export function TimeField({ className = "", ...props }) {
4
+ export function TimeField({ className = "", value, defaultValue, onChange, ...props }) {
5
+ const isControlled = value != null;
6
+ const [internal, setInternal] = useState(defaultValue ?? "");
7
+ const current = isControlled ? value : internal;
8
+ // Mute the native --:-- format placeholder while the field is empty.
9
+ const emptyClass = current ? "" : "a1-field--mask-empty";
10
+
11
+ function handleChange(event) {
12
+ if (!isControlled) setInternal(event.target.value);
13
+ onChange?.(event);
14
+ }
15
+
4
16
  return (
5
17
  <TextField
6
18
  type="time"
7
- className={`a1-field--fit ${className}`.trim()}
19
+ className={`a1-field--fit ${emptyClass} ${className}`.replace(/\s+/g, " ").trim()}
20
+ value={isControlled ? value : undefined}
21
+ defaultValue={isControlled ? undefined : defaultValue}
22
+ onChange={handleChange}
8
23
  {...props}
9
24
  />
10
25
  );
@@ -7,7 +7,7 @@
7
7
  --a1-field-gap: var(--component-field-default-gap);
8
8
  --a1-field-border-radius: var(--base-radius-md);
9
9
  --a1-field-font-size: var(--semantic-font-size-body-md);
10
- --a1-field-label-size: var(--semantic-font-size-body-sm);
10
+ --a1-field-label-size: var(--semantic-font-size-form-label-default);
11
11
  --a1-field-label-weight: var(--component-field-label-font-weight);
12
12
  --a1-field-message-size: var(--semantic-font-size-body-xs);
13
13
  --a1-field-chevron-size: var(--component-field-chevron-size);
@@ -34,7 +34,7 @@
34
34
  --a1-field-padding-inline: var(--component-field-comfortable-padding-inline);
35
35
  --a1-field-gap: var(--component-field-comfortable-gap);
36
36
  --a1-field-border-radius: var(--base-radius-lg);
37
- --a1-field-label-size: var(--semantic-font-size-body-md);
37
+ --a1-field-label-size: var(--semantic-font-size-form-label-comfortable);
38
38
  --a1-field-message-size: var(--semantic-font-size-body-sm);
39
39
  --a1-field-chevron-size: var(--component-field-chevron-size-comfortable);
40
40
  --a1-field-side-label-width: var(--component-field-side-label-width-comfortable);
@@ -66,7 +66,7 @@
66
66
  --a1-field-gap: var(--component-field-compact-gap);
67
67
  --a1-field-border-radius: var(--base-radius-sm);
68
68
  --a1-field-font-size: var(--semantic-font-size-body-sm);
69
- --a1-field-label-size: var(--semantic-font-size-body-xs);
69
+ --a1-field-label-size: var(--semantic-font-size-form-label-compact);
70
70
  --a1-field-label-weight: var(--component-field-compact-label-font-weight);
71
71
  --a1-field-message-size: var(--semantic-font-size-body-xs);
72
72
  --a1-field-chevron-size: var(--component-field-chevron-size-compact);
@@ -170,13 +170,12 @@
170
170
 
171
171
  /* ─── Active ───────────────────────────────────────────────────────────────── */
172
172
 
173
+ /* Active keeps the border feedback only — no background colour change. */
173
174
  :is(.a1-field__input, .a1-field__select):active:not(:disabled) {
174
- background: var(--a1-field-active-background);
175
175
  border-color: var(--a1-field-active-border-color);
176
176
  }
177
177
 
178
178
  .a1-field__input:read-only:active:not(:disabled) {
179
- background: var(--a1-field-read-only-background);
180
179
  border-color: var(--a1-field-read-only-border-color);
181
180
  }
182
181
 
@@ -269,6 +268,14 @@
269
268
  cursor: not-allowed;
270
269
  }
271
270
 
271
+ /* Mute the native date/time format placeholder (mm/dd/yyyy, --:--) while the
272
+ field is empty, matching the muted mask placeholder used by Phone/Zip/Card.
273
+ DateField/TimeField add a1-field--mask-empty until a value is entered. */
274
+ .a1-field--mask-empty .a1-field__input[type="date"]::-webkit-datetime-edit,
275
+ .a1-field--mask-empty .a1-field__input[type="time"]::-webkit-datetime-edit {
276
+ color: var(--semantic-color-text-muted);
277
+ }
278
+
272
279
  /* ─── Fit-content width (date, zip, and other fixed-width fields) ───────────── */
273
280
 
274
281
  .a1-field--fit {
@@ -55,13 +55,12 @@
55
55
 
56
56
  /* ─── Active ─────────────────────────────────────────────────────────────── */
57
57
 
58
+ /* Active keeps the border feedback only — no background colour change. */
58
59
  .a1-field__textarea:active:not(:disabled) {
59
- background: var(--a1-field-active-background);
60
60
  border-color: var(--a1-field-active-border-color);
61
61
  }
62
62
 
63
63
  .a1-field__textarea:read-only:active:not(:disabled) {
64
- background: var(--a1-field-read-only-background);
65
64
  border-color: var(--a1-field-read-only-border-color);
66
65
  }
67
66
 
@@ -5,6 +5,8 @@
5
5
  margin: 0;
6
6
  padding: 0;
7
7
  min-width: 0; /* prevents default overflow on narrow containers */
8
+ inline-size: 100%; /* fill the container regardless of flex/grid/intrinsic sizing */
9
+ box-sizing: border-box;
8
10
  }
9
11
 
10
12
  /* ─── Legend ─────────────────────────────────────────────────────────────── */
@@ -1,6 +1,12 @@
1
1
  import * as React from "react";
2
2
 
3
3
  export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
+ /**
5
+ * Element or component to render as. Use `as="a"` (with `href`) to render the
6
+ * icon button as a navigation link while keeping its visual styling.
7
+ * Default: "button"
8
+ */
9
+ as?: React.ElementType;
4
10
  /** Material Symbols icon name */
5
11
  icon: string;
6
12
  /** Accessible label (used as `aria-label` and visible tooltip) */
@@ -9,6 +15,8 @@ export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonEl
9
15
  variant?: "tertiary" | "secondary" | "destructive" | "success";
10
16
  /** Button size. "lg" matches Button's large touch target (3.5rem) and icon size, suitable for pairing with large Buttons. Default: "md" */
11
17
  size?: "md" | "lg";
18
+ /** Link target when rendered with `as="a"`. */
19
+ href?: string;
12
20
  disabled?: boolean;
13
21
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
14
22
  }
@@ -5,6 +5,7 @@ const variants = ["tertiary", "secondary", "destructive", "success"];
5
5
  const sizes = ["md", "lg"];
6
6
 
7
7
  export function IconButton({
8
+ as: Component = "button",
8
9
  icon,
9
10
  label,
10
11
  variant = "tertiary",
@@ -16,6 +17,9 @@ export function IconButton({
16
17
  }) {
17
18
  const resolvedVariant = variants.includes(variant) ? variant : "tertiary";
18
19
  const resolvedSize = sizes.includes(size) ? size : null;
20
+ // When rendered as a link (as="a") the native `disabled` attribute does not
21
+ // apply, so fall back to aria-disabled for assistive tech.
22
+ const isButton = Component === "button";
19
23
  const classes = [
20
24
  "a1-icon-button",
21
25
  `a1-icon-button--${resolvedVariant}`,
@@ -24,15 +28,16 @@ export function IconButton({
24
28
  ].filter(Boolean).join(" ");
25
29
 
26
30
  return (
27
- <button
28
- type="button"
31
+ <Component
32
+ type={isButton ? "button" : undefined}
29
33
  className={classes}
30
34
  aria-label={label}
31
- disabled={disabled}
35
+ disabled={isButton ? disabled : undefined}
36
+ aria-disabled={!isButton && disabled ? "true" : undefined}
32
37
  onClick={onClick}
33
38
  {...props}
34
39
  >
35
40
  <Icon name={icon} />
36
- </button>
41
+ </Component>
37
42
  );
38
43
  }
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+
3
+ export interface InlineEditableProps
4
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "children"> {
5
+ /** Current text value (controlled). */
6
+ value: string;
7
+ /** Called with the new value as the user types. */
8
+ onChange: (value: string) => void;
9
+ /** Edit in a `<textarea>` instead of a single-line `<input>`. Default: false */
10
+ multiline?: boolean;
11
+ /** Prevents entering edit mode and removes the interactive affordances. Default: false */
12
+ disabled?: boolean;
13
+ /** Edit the text in place via `contentEditable` instead of a boxed field. The element inherits all typography (font, size, colour, line-height, alignment, wrapping) from the surrounding component, so editing never resizes or restyles the text — ideal for making any heading, paragraph, label, or button text live-editable. Only a focus ring is added. Default: false */
14
+ seamless?: boolean;
15
+ /** Text shown in the display state when there is no value and no `children`. */
16
+ placeholder?: string;
17
+ /** Class applied to the wrapper in the display state. */
18
+ className?: string;
19
+ /** Class applied to the `<input>` / `<textarea>` in the edit state. */
20
+ inputClassName?: string;
21
+ /** Display content rendered in the read state; falls back to `placeholder` when omitted. */
22
+ children?: React.ReactNode;
23
+ }
24
+
25
+ export declare function InlineEditable(props: InlineEditableProps): React.ReactElement;