@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.
- package/package.json +1 -1
- package/src/components/accordion/Accordion.jsx +2 -0
- package/src/components/banner/Banner.jsx +4 -1
- package/src/components/blockquote/blockquote.css +0 -2
- package/src/components/bottom-drawer/BottomDrawer.jsx +2 -2
- package/src/components/button/Button.d.ts +4 -0
- package/src/components/button/Button.jsx +15 -3
- package/src/components/button/button.css +39 -0
- package/src/components/calendar/calendar.css +0 -2
- package/src/components/card/card.css +1 -0
- package/src/components/checkbox-group/CheckboxGroup.jsx +1 -1
- package/src/components/checkbox-group/checkbox-group.css +3 -3
- package/src/components/choice-group/ChoiceGroup.d.ts +23 -0
- package/src/components/choice-group/ChoiceGroup.jsx +22 -10
- package/src/components/choice-group/choice-group.css +53 -8
- package/src/components/code/Code.d.ts +4 -0
- package/src/components/code/Code.jsx +44 -8
- package/src/components/code/code.css +29 -0
- package/src/components/context-menu/ContextMenu.d.ts +56 -0
- package/src/components/context-menu/ContextMenu.jsx +146 -0
- package/src/components/context-menu/context-menu.css +107 -0
- package/src/components/data-table/DataTable.jsx +1 -1
- package/src/components/definition-list/definition-list.css +15 -0
- package/src/components/divider/Divider.d.ts +4 -2
- package/src/components/divider/Divider.jsx +6 -1
- package/src/components/divider/divider.css +9 -5
- package/src/components/field/DateField.jsx +17 -2
- package/src/components/field/SelectField.jsx +1 -1
- package/src/components/field/TextField.d.ts +2 -0
- package/src/components/field/TextField.jsx +1 -1
- package/src/components/field/TextareaField.jsx +1 -1
- package/src/components/field/TimeField.jsx +17 -2
- package/src/components/field/field.css +12 -5
- package/src/components/field/textarea-field.css +1 -2
- package/src/components/fieldset/fieldset.css +2 -0
- package/src/components/icon-button/IconButton.d.ts +8 -0
- package/src/components/icon-button/IconButton.jsx +9 -4
- package/src/components/inline-editable/InlineEditable.d.ts +25 -0
- package/src/components/inline-editable/InlineEditable.jsx +77 -1
- package/src/components/inline-editable/inline-editable.css +44 -1
- package/src/components/message/Message.jsx +15 -9
- package/src/components/page-layout/page-layout.css +13 -0
- package/src/components/page-nav/page-nav.css +0 -2
- package/src/components/pagination/Pagination.jsx +3 -1
- package/src/components/radio-group/RadioGroup.jsx +1 -1
- package/src/components/radio-group/radio-group.css +3 -3
- package/src/components/section/Section.d.ts +8 -0
- package/src/components/section/Section.jsx +24 -0
- package/src/components/section/section.css +28 -0
- package/src/components/snackbar/Snackbar.d.ts +24 -0
- package/src/components/snackbar/Snackbar.jsx +11 -8
- package/src/components/snackbar/snackbar.css +7 -22
- package/src/components/stack/Stack.jsx +2 -1
- package/src/components/sticky-actions/StickyActions.d.ts +7 -0
- package/src/components/sticky-actions/StickyActions.jsx +23 -4
- package/src/components/sticky-actions/sticky-actions.css +5 -3
- package/src/components/tabs/Tabs.d.ts +2 -0
- package/src/components/tabs/Tabs.jsx +3 -3
- package/src/components/tabs/tabs.css +95 -0
- package/src/components/top-header/TopHeader.jsx +2 -0
- package/src/components/tree-menu/TreeMenu.d.ts +54 -0
- package/src/components/tree-menu/TreeMenu.jsx +500 -0
- package/src/components/tree-menu/tree-menu.css +254 -0
- package/src/index.js +2 -0
- 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
|
-
/**
|
|
13
|
-
variant?: "subtle" | "strong" | "accent"
|
|
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"
|
|
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 {
|
|
155
|
-
.a1-divider--strong {
|
|
156
|
-
.a1-divider--accent {
|
|
157
|
-
|
|
158
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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;
|