@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.
- 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 -7
- 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/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
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ export function Accordion({
|
|
|
13
13
|
size = "md",
|
|
14
14
|
disabled = false,
|
|
15
15
|
className = "",
|
|
16
|
+
...rest
|
|
16
17
|
}) {
|
|
17
18
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
18
19
|
const isControlled = controlledOpen !== undefined;
|
|
@@ -48,6 +49,7 @@ export function Accordion({
|
|
|
48
49
|
disabled && "a1-accordion--disabled",
|
|
49
50
|
className,
|
|
50
51
|
].filter(Boolean).join(" ")}
|
|
52
|
+
{...rest}
|
|
51
53
|
>
|
|
52
54
|
<button
|
|
53
55
|
id={triggerId}
|
|
@@ -28,7 +28,9 @@ export function Banner({
|
|
|
28
28
|
icon,
|
|
29
29
|
action,
|
|
30
30
|
onDismiss,
|
|
31
|
+
className = "",
|
|
31
32
|
children,
|
|
33
|
+
...rest
|
|
32
34
|
}) {
|
|
33
35
|
const resolvedVariant = VARIANTS.includes(variant) ? variant : "inline";
|
|
34
36
|
const resolvedStatus = STATUSES.includes(status) ? status : "neutral";
|
|
@@ -36,9 +38,10 @@ export function Banner({
|
|
|
36
38
|
|
|
37
39
|
return (
|
|
38
40
|
<div
|
|
39
|
-
className={`a1-banner a1-banner--${resolvedVariant} a1-banner--${resolvedStatus}`}
|
|
41
|
+
className={`a1-banner a1-banner--${resolvedVariant} a1-banner--${resolvedStatus}${className ? ` ${className}` : ""}`}
|
|
40
42
|
role="alert"
|
|
41
43
|
aria-live="polite"
|
|
44
|
+
{...rest}
|
|
42
45
|
>
|
|
43
46
|
<div className="a1-banner__inner">
|
|
44
47
|
<span className="a1-banner__icon" aria-hidden="true">
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Icon } from "../icon/Icon.jsx";
|
|
2
2
|
import "./bottom-drawer.css";
|
|
3
3
|
|
|
4
|
-
export function BottomDrawer({ items = [], "aria-label": ariaLabel = "Primary navigation", className = "" }) {
|
|
4
|
+
export function BottomDrawer({ items = [], "aria-label": ariaLabel = "Primary navigation", className = "", ...rest }) {
|
|
5
5
|
const visibleItems = items.slice(0, 5);
|
|
6
6
|
|
|
7
7
|
return (
|
|
8
|
-
<nav className={["a1-bottom-drawer", className].filter(Boolean).join(" ")} aria-label={ariaLabel}>
|
|
8
|
+
<nav className={["a1-bottom-drawer", className].filter(Boolean).join(" ")} aria-label={ariaLabel} {...rest}>
|
|
9
9
|
<ul className="a1-bottom-drawer__list" role="list">
|
|
10
10
|
{visibleItems.map((item) => {
|
|
11
11
|
const Tag = item.href ? "a" : "button";
|
|
@@ -11,6 +11,10 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
11
11
|
icon?: string;
|
|
12
12
|
/** Whether the icon appears before or after the label. Default: "start" */
|
|
13
13
|
iconPosition?: "start" | "end";
|
|
14
|
+
/** Stretch the button to fill the width of its container. When false the button uses its natural content width. Default: false */
|
|
15
|
+
fullWidth?: boolean;
|
|
16
|
+
/** Show a loading spinner (replacing the icon) and make the button inert (disabled + aria-busy). Use while an action is in progress, e.g. submitting a form. Default: false */
|
|
17
|
+
loading?: boolean;
|
|
14
18
|
children?: React.ReactNode;
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -11,35 +11,47 @@ export function Button({
|
|
|
11
11
|
size = "md",
|
|
12
12
|
icon,
|
|
13
13
|
iconPosition = "start",
|
|
14
|
+
fullWidth = false,
|
|
15
|
+
loading = false,
|
|
14
16
|
className = "",
|
|
15
17
|
type,
|
|
18
|
+
disabled,
|
|
16
19
|
children,
|
|
17
20
|
...props
|
|
18
21
|
}) {
|
|
19
22
|
const resolvedVariant = variants.includes(variant) ? variant : "primary";
|
|
20
23
|
const resolvedSize = sizes.includes(size) ? size : "md";
|
|
21
24
|
const resolvedPosition = iconPositions.includes(iconPosition) ? iconPosition : "start";
|
|
25
|
+
const isButton = Component === "button";
|
|
26
|
+
const isInert = disabled || loading;
|
|
22
27
|
const classes = [
|
|
23
28
|
"a1-button",
|
|
24
29
|
`a1-button--${resolvedVariant}`,
|
|
25
30
|
resolvedSize !== "md" && `a1-button--${resolvedSize}`,
|
|
26
31
|
icon && "a1-button--has-icon",
|
|
32
|
+
fullWidth && "a1-button--full-width",
|
|
33
|
+
loading && "a1-button--loading",
|
|
27
34
|
className
|
|
28
35
|
]
|
|
29
36
|
.filter(Boolean)
|
|
30
37
|
.join(" ");
|
|
31
38
|
|
|
32
39
|
const iconEl = icon ? <Icon name={icon} className="a1-button__icon" /> : null;
|
|
40
|
+
// While loading, a spinner replaces the icon and the button becomes inert.
|
|
41
|
+
const spinnerEl = loading ? <span className="a1-button__spinner" aria-hidden="true" /> : null;
|
|
33
42
|
|
|
34
43
|
return (
|
|
35
44
|
<Component
|
|
36
45
|
className={classes}
|
|
37
|
-
type={
|
|
46
|
+
type={isButton ? type ?? "button" : type}
|
|
47
|
+
disabled={isButton ? isInert || undefined : undefined}
|
|
48
|
+
aria-disabled={!isButton && isInert ? "true" : undefined}
|
|
49
|
+
aria-busy={loading ? "true" : undefined}
|
|
38
50
|
{...props}
|
|
39
51
|
>
|
|
40
|
-
{resolvedPosition === "start" && iconEl}
|
|
52
|
+
{loading ? spinnerEl : resolvedPosition === "start" && iconEl}
|
|
41
53
|
{children}
|
|
42
|
-
{resolvedPosition === "end" && iconEl}
|
|
54
|
+
{!loading && resolvedPosition === "end" && iconEl}
|
|
43
55
|
</Component>
|
|
44
56
|
);
|
|
45
57
|
}
|
|
@@ -62,6 +62,45 @@
|
|
|
62
62
|
pointer-events: none;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/* Full-width: stretch to fill the container. Default buttons stay inline-flex
|
|
66
|
+
(natural content width). */
|
|
67
|
+
.a1-button--full-width {
|
|
68
|
+
display: flex;
|
|
69
|
+
width: 100%;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Loading: the button is inert but reads as working (not disabled). A spinner
|
|
73
|
+
replaces the icon; full opacity distinguishes it from the disabled state. */
|
|
74
|
+
.a1-button--loading {
|
|
75
|
+
cursor: progress;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.a1-button--loading:disabled,
|
|
79
|
+
.a1-button--loading[aria-disabled="true"] {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.a1-button__spinner {
|
|
85
|
+
inline-size: var(--component-button-icon-size);
|
|
86
|
+
block-size: var(--component-button-icon-size);
|
|
87
|
+
flex-shrink: 0;
|
|
88
|
+
border: var(--base-spacing-2) solid currentColor;
|
|
89
|
+
border-top-color: transparent;
|
|
90
|
+
border-radius: var(--base-radius-pill);
|
|
91
|
+
animation: a1-button-spin var(--semantic-motion-duration-slowest) linear infinite;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@keyframes a1-button-spin {
|
|
95
|
+
to { transform: rotate(360deg); }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (prefers-reduced-motion: reduce) {
|
|
99
|
+
.a1-button__spinner {
|
|
100
|
+
animation-duration: calc(var(--semantic-motion-duration-slowest) * 3);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
65
104
|
.a1-button--primary {
|
|
66
105
|
--a1-button-background: var(--component-button-primary-background);
|
|
67
106
|
--a1-button-background-hover: var(--component-button-primary-background-hover);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
.a1-card {
|
|
2
2
|
container: a1-card / inline-size;
|
|
3
3
|
box-sizing: border-box;
|
|
4
|
+
inline-size: 100%;
|
|
4
5
|
background: var(--semantic-color-surface-card);
|
|
5
6
|
border: var(--component-card-border-width) solid var(--semantic-color-border-subtle);
|
|
6
7
|
border-radius: var(--component-card-border-radius);
|
|
@@ -64,7 +64,7 @@ export function CheckboxGroup({
|
|
|
64
64
|
<span className="a1-checkbox-group__legend-inner">
|
|
65
65
|
{label}
|
|
66
66
|
{required && resolvedSize === "comfortable" ? (
|
|
67
|
-
<MessageBadge status="info" subtle>{requiredText}</MessageBadge>
|
|
67
|
+
<MessageBadge status="info" subtle size="sm" icon={null}>{requiredText}</MessageBadge>
|
|
68
68
|
) : required ? (
|
|
69
69
|
<span className="a1-field__asterisk" aria-hidden="true"> *</span>
|
|
70
70
|
) : null}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
--a1-cb-input-nudge: var(--component-checkbox-group-input-nudge); /* top margin aligning box center with label cap-height */
|
|
17
17
|
--a1-cb-row-py: var(--component-checkbox-group-row-padding-block); /* vertical padding on each item row */
|
|
18
18
|
--a1-cb-row-px: var(--component-checkbox-group-row-padding-inline); /* horizontal padding on each item row */
|
|
19
|
-
--a1-cb-legend-size: var(--semantic-font-size-
|
|
19
|
+
--a1-cb-legend-size: var(--semantic-font-size-form-label-default);
|
|
20
20
|
--a1-cb-label-size: var(--semantic-font-size-body-md);
|
|
21
21
|
--a1-cb-hint-size: var(--semantic-font-size-body-xs);
|
|
22
22
|
--a1-cb-msg-size: var(--semantic-font-size-body-xs);
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
--a1-cb-input-nudge: var(--component-checkbox-group-comfortable-input-nudge);
|
|
38
38
|
--a1-cb-row-py: var(--component-checkbox-group-comfortable-row-padding-block);
|
|
39
39
|
--a1-cb-row-px: var(--component-checkbox-group-comfortable-row-padding-inline);
|
|
40
|
-
--a1-cb-legend-size: var(--semantic-font-size-
|
|
40
|
+
--a1-cb-legend-size: var(--semantic-font-size-form-label-comfortable);
|
|
41
41
|
--a1-cb-label-size: var(--semantic-font-size-body-md);
|
|
42
42
|
--a1-cb-hint-size: var(--semantic-font-size-body-sm);
|
|
43
43
|
--a1-cb-msg-size: var(--semantic-font-size-body-sm);
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
--a1-cb-input-nudge: var(--component-checkbox-group-compact-input-nudge);
|
|
66
66
|
--a1-cb-row-py: var(--component-checkbox-group-compact-row-padding-block);
|
|
67
67
|
--a1-cb-row-px: var(--component-checkbox-group-compact-row-padding-inline);
|
|
68
|
-
--a1-cb-legend-size: var(--semantic-font-size-
|
|
68
|
+
--a1-cb-legend-size: var(--semantic-font-size-form-label-compact);
|
|
69
69
|
--a1-cb-label-size: var(--semantic-font-size-body-sm);
|
|
70
70
|
--a1-cb-hint-size: var(--semantic-font-size-body-xs);
|
|
71
71
|
--a1-cb-msg-size: var(--semantic-font-size-body-xs);
|
|
@@ -11,7 +11,18 @@ export interface ChoiceOption {
|
|
|
11
11
|
value: string;
|
|
12
12
|
label: string;
|
|
13
13
|
subtext?: string;
|
|
14
|
+
/** Material Symbols icon name. Mutually exclusive with swatch — swatch takes precedence. */
|
|
14
15
|
icon?: string;
|
|
16
|
+
/**
|
|
17
|
+
* CSS color value rendered as a filled circle swatch instead of an icon.
|
|
18
|
+
* Accepts any CSS color including custom properties, e.g. "var(--semantic-color-action-background)".
|
|
19
|
+
*/
|
|
20
|
+
swatch?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Visually hide this tile's label/subtext, showing only the icon. The label
|
|
23
|
+
* is still rendered for screen readers. Requires `icon` to be set. Default: false
|
|
24
|
+
*/
|
|
25
|
+
iconOnly?: boolean;
|
|
15
26
|
disabled?: boolean;
|
|
16
27
|
}
|
|
17
28
|
|
|
@@ -42,6 +53,18 @@ export interface ChoiceGroupProps {
|
|
|
42
53
|
* above the content block. Has no effect on tiles with no icon. Default: false
|
|
43
54
|
*/
|
|
44
55
|
inlineIcon?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Hide the radio/checkbox selection indicator from all tiles. Selection state is
|
|
58
|
+
* still communicated via border, background, and the accessible input.
|
|
59
|
+
* Useful for compact configuration controls. Default: false
|
|
60
|
+
*/
|
|
61
|
+
hideIndicator?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Visually hide the label and subtext, showing only the icon. The label is still
|
|
64
|
+
* rendered in the DOM for screen readers. Requires each option to have both
|
|
65
|
+
* `icon` and `label`. Default: false
|
|
66
|
+
*/
|
|
67
|
+
iconOnly?: boolean;
|
|
45
68
|
required?: boolean;
|
|
46
69
|
/** Input name attribute. Defaults to the group id. */
|
|
47
70
|
name?: string;
|
|
@@ -13,19 +13,21 @@ export function ChoiceGroup({
|
|
|
13
13
|
hint,
|
|
14
14
|
error,
|
|
15
15
|
success,
|
|
16
|
-
size
|
|
16
|
+
size = "default",
|
|
17
17
|
columns,
|
|
18
|
-
multiple
|
|
19
|
-
inlineIcon
|
|
20
|
-
|
|
18
|
+
multiple = false,
|
|
19
|
+
inlineIcon = false,
|
|
20
|
+
hideIndicator = false,
|
|
21
|
+
iconOnly = false,
|
|
22
|
+
required = false,
|
|
21
23
|
name,
|
|
22
|
-
options
|
|
24
|
+
options = [],
|
|
23
25
|
sections,
|
|
24
26
|
value,
|
|
25
27
|
defaultValue,
|
|
26
28
|
onChange,
|
|
27
29
|
id: providedId,
|
|
28
|
-
className
|
|
30
|
+
className = "",
|
|
29
31
|
...props
|
|
30
32
|
}) {
|
|
31
33
|
const autoId = useId();
|
|
@@ -77,7 +79,9 @@ export function ChoiceGroup({
|
|
|
77
79
|
"a1-choice-group",
|
|
78
80
|
resolvedSize !== "default" && `a1-choice-group--${resolvedSize}`,
|
|
79
81
|
multiple ? "a1-choice-group--multiple" : "a1-choice-group--single",
|
|
80
|
-
inlineIcon
|
|
82
|
+
inlineIcon && "a1-choice-group--inline-icon",
|
|
83
|
+
hideIndicator && "a1-choice-group--no-indicator",
|
|
84
|
+
iconOnly && "a1-choice-group--icon-only",
|
|
81
85
|
isFixedColumns && "a1-choice-group--fixed-columns",
|
|
82
86
|
responsiveClass,
|
|
83
87
|
error && "a1-choice-group--error",
|
|
@@ -105,7 +109,8 @@ export function ChoiceGroup({
|
|
|
105
109
|
htmlFor={itemId}
|
|
106
110
|
className={[
|
|
107
111
|
"a1-choice-item",
|
|
108
|
-
isDisabled
|
|
112
|
+
isDisabled && "a1-choice-item--disabled",
|
|
113
|
+
option.iconOnly && "a1-choice-item--icon-only",
|
|
109
114
|
].filter(Boolean).join(" ")}
|
|
110
115
|
>
|
|
111
116
|
<input
|
|
@@ -118,7 +123,14 @@ export function ChoiceGroup({
|
|
|
118
123
|
disabled={isDisabled}
|
|
119
124
|
onChange={(e) => handleChange(option.value, e.target.checked)}
|
|
120
125
|
/>
|
|
121
|
-
{option.
|
|
126
|
+
{option.swatch && (
|
|
127
|
+
<span
|
|
128
|
+
className="a1-choice-item__swatch"
|
|
129
|
+
aria-hidden="true"
|
|
130
|
+
style={{ "--a1-swatch-color": option.swatch }}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
{!option.swatch && option.icon && (
|
|
122
134
|
<span className="a1-choice-item__icon" aria-hidden="true">
|
|
123
135
|
<Icon name={option.icon} />
|
|
124
136
|
</span>
|
|
@@ -160,7 +172,7 @@ export function ChoiceGroup({
|
|
|
160
172
|
<span className="a1-choice-group__legend-inner">
|
|
161
173
|
{label}
|
|
162
174
|
{required && resolvedSize === "comfortable" ? (
|
|
163
|
-
<MessageBadge status="info" subtle>{requiredText}</MessageBadge>
|
|
175
|
+
<MessageBadge status="info" subtle size="sm" icon={null}>{requiredText}</MessageBadge>
|
|
164
176
|
) : required ? (
|
|
165
177
|
<span className="a1-field__asterisk" aria-hidden="true"> *</span>
|
|
166
178
|
) : null}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
margin: 0;
|
|
6
6
|
padding: 0;
|
|
7
7
|
min-width: 0;
|
|
8
|
+
inline-size: 100%; /* fill the container regardless of flex/grid/intrinsic sizing */
|
|
9
|
+
box-sizing: border-box;
|
|
8
10
|
|
|
9
11
|
/* Tile size defaults (default) */
|
|
10
12
|
--a1-cg-padding: var(--component-choice-group-default-padding);
|
|
@@ -12,9 +14,9 @@
|
|
|
12
14
|
--a1-cg-indicator-size: var(--component-choice-group-default-indicator-size);
|
|
13
15
|
--a1-cg-content-gap: var(--component-choice-group-default-content-gap);
|
|
14
16
|
--a1-cg-min-width: var(--component-choice-group-default-min-width);
|
|
15
|
-
--a1-cg-label-size: var(--semantic-font-size-body-
|
|
16
|
-
--a1-cg-subtext-size: var(--semantic-font-size-body-
|
|
17
|
-
--a1-cg-legend-size: var(--semantic-font-size-
|
|
17
|
+
--a1-cg-label-size: var(--semantic-font-size-body-sm);
|
|
18
|
+
--a1-cg-subtext-size: var(--semantic-font-size-body-xs);
|
|
19
|
+
--a1-cg-legend-size: var(--semantic-font-size-form-label-default);
|
|
18
20
|
--a1-cg-section-label-size: var(--semantic-font-size-body-xs);
|
|
19
21
|
|
|
20
22
|
/* Gap between tiles — tracks size */
|
|
@@ -37,9 +39,10 @@
|
|
|
37
39
|
--a1-cg-indicator-size: var(--component-choice-group-compact-indicator-size);
|
|
38
40
|
--a1-cg-content-gap: var(--component-choice-group-compact-content-gap);
|
|
39
41
|
--a1-cg-min-width: var(--component-choice-group-compact-min-width);
|
|
40
|
-
|
|
41
|
-
--a1-cg-
|
|
42
|
-
--a1-cg-
|
|
42
|
+
/* Compact reduces label and subtext one step below the default size. */
|
|
43
|
+
--a1-cg-label-size: var(--semantic-font-size-body-xs);
|
|
44
|
+
--a1-cg-subtext-size: var(--semantic-font-size-body-2xs);
|
|
45
|
+
--a1-cg-legend-size: var(--semantic-font-size-form-label-compact);
|
|
43
46
|
--a1-cg-gap: var(--component-choice-group-gap-sm);
|
|
44
47
|
--a1-cg-group-gap: var(--component-choice-group-compact-group-gap);
|
|
45
48
|
--a1-cg-items-top-gap: var(--component-choice-group-compact-items-top-gap);
|
|
@@ -53,7 +56,7 @@
|
|
|
53
56
|
--a1-cg-min-width: var(--component-choice-group-comfortable-min-width);
|
|
54
57
|
--a1-cg-label-size: var(--semantic-font-size-body-lg);
|
|
55
58
|
--a1-cg-subtext-size: var(--semantic-font-size-body-md);
|
|
56
|
-
--a1-cg-legend-size: var(--semantic-font-size-
|
|
59
|
+
--a1-cg-legend-size: var(--semantic-font-size-form-label-comfortable);
|
|
57
60
|
--a1-cg-section-label-size: var(--semantic-font-size-body-sm);
|
|
58
61
|
--a1-cg-gap: var(--component-choice-group-gap-lg);
|
|
59
62
|
--a1-cg-group-gap: var(--component-choice-group-comfortable-group-gap);
|
|
@@ -327,6 +330,49 @@
|
|
|
327
330
|
flex-shrink: 0;
|
|
328
331
|
}
|
|
329
332
|
|
|
333
|
+
/* ─── No indicator ──────────────────────────────────────────────────────────── */
|
|
334
|
+
|
|
335
|
+
.a1-choice-group--no-indicator .a1-choice-item {
|
|
336
|
+
padding-inline-start: var(--a1-cg-padding);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.a1-choice-group--no-indicator .a1-choice-item__indicator {
|
|
340
|
+
display: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* ─── Icon only (group-level and per-option) ────────────────────────────────── */
|
|
344
|
+
|
|
345
|
+
.a1-choice-group--icon-only .a1-choice-item,
|
|
346
|
+
.a1-choice-item--icon-only {
|
|
347
|
+
align-items: center;
|
|
348
|
+
justify-content: center;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.a1-choice-group--icon-only .a1-choice-item__content,
|
|
352
|
+
.a1-choice-item--icon-only .a1-choice-item__content {
|
|
353
|
+
position: absolute;
|
|
354
|
+
width: 1px;
|
|
355
|
+
height: 1px;
|
|
356
|
+
padding: 0;
|
|
357
|
+
margin: -1px;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
clip: rect(0, 0, 0, 0);
|
|
360
|
+
white-space: nowrap;
|
|
361
|
+
border: 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ─── Swatch ────────────────────────────────────────────────────────────────── */
|
|
365
|
+
|
|
366
|
+
.a1-choice-item__swatch {
|
|
367
|
+
display: block;
|
|
368
|
+
width: var(--base-spacing-20);
|
|
369
|
+
height: var(--base-spacing-20);
|
|
370
|
+
border-radius: var(--base-radius-sm);
|
|
371
|
+
background-color: var(--a1-swatch-color, var(--semantic-color-text-default));
|
|
372
|
+
border: var(--base-spacing-1) solid var(--semantic-color-border-subtle);
|
|
373
|
+
flex-shrink: 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
330
376
|
/* ─── Error state ───────────────────────────────────────────────────────────── */
|
|
331
377
|
|
|
332
378
|
.a1-choice-group--error
|
|
@@ -9,6 +9,10 @@ export interface CodeProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
9
9
|
copyCode?: boolean;
|
|
10
10
|
/** Text copied to the clipboard. Defaults to the rendered text children. */
|
|
11
11
|
copyText?: string;
|
|
12
|
+
/** Render the block as an editable textarea initialized from children. Only meaningful in block mode. Default: false */
|
|
13
|
+
editable?: boolean;
|
|
14
|
+
/** Called with the current string value whenever the editable textarea changes. */
|
|
15
|
+
onChangeValue?: (value: string) => void;
|
|
12
16
|
children?: React.ReactNode;
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -54,20 +54,34 @@ export function Code({
|
|
|
54
54
|
wrapping = false,
|
|
55
55
|
copyCode = false,
|
|
56
56
|
copyText,
|
|
57
|
+
editable = false,
|
|
58
|
+
onChangeValue,
|
|
57
59
|
className = "",
|
|
58
60
|
children,
|
|
59
61
|
...props
|
|
60
62
|
}) {
|
|
61
63
|
const resolvedVariant = variants.includes(variant) ? variant : "inline";
|
|
62
64
|
const [copied, setCopied] = useState(false);
|
|
65
|
+
const [editableValue, setEditableValue] = useState(() =>
|
|
66
|
+
textFromChildren(Children.toArray(children))
|
|
67
|
+
);
|
|
63
68
|
const resetTimer = useRef(null);
|
|
69
|
+
|
|
70
|
+
// Keep the textarea in sync when children change from outside (e.g. undo/redo).
|
|
71
|
+
// React's Object.is bail-out means this is a no-op while the user is typing
|
|
72
|
+
// (children and editableValue are already equal after each keystroke).
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!editable) return;
|
|
75
|
+
setEditableValue(textFromChildren(Children.toArray(children)));
|
|
76
|
+
}, [children, editable]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
77
|
+
|
|
64
78
|
const copyLabel = useLabel("code.copyCode", "Copy code");
|
|
65
79
|
const copiedLabel = useLabel("code.copied", "Copied");
|
|
66
80
|
const textToCopy = useMemo(
|
|
67
|
-
() => copyText || textFromChildren(Children.toArray(children)),
|
|
68
|
-
[children, copyText],
|
|
81
|
+
() => copyText || (editable ? editableValue : textFromChildren(Children.toArray(children))),
|
|
82
|
+
[children, copyText, editable, editableValue],
|
|
69
83
|
);
|
|
70
|
-
const shouldRenderBlock = resolvedVariant === "block" || copyCode;
|
|
84
|
+
const shouldRenderBlock = resolvedVariant === "block" || copyCode || editable;
|
|
71
85
|
|
|
72
86
|
useEffect(() => {
|
|
73
87
|
return () => {
|
|
@@ -75,6 +89,11 @@ export function Code({
|
|
|
75
89
|
};
|
|
76
90
|
}, []);
|
|
77
91
|
|
|
92
|
+
function handleTextareaChange(e) {
|
|
93
|
+
setEditableValue(e.target.value);
|
|
94
|
+
onChangeValue?.(e.target.value);
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
async function handleCopy() {
|
|
79
98
|
await writeClipboard(textToCopy);
|
|
80
99
|
setCopied(true);
|
|
@@ -108,16 +127,33 @@ export function Code({
|
|
|
108
127
|
className={[
|
|
109
128
|
"a1-code-block",
|
|
110
129
|
copyCode && "a1-code-block--copyable",
|
|
130
|
+
editable && "a1-code-block--editable",
|
|
111
131
|
className,
|
|
112
132
|
]
|
|
113
133
|
.filter(Boolean)
|
|
114
134
|
.join(" ")}
|
|
115
135
|
>
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
{editable ? (
|
|
137
|
+
<textarea
|
|
138
|
+
className={[
|
|
139
|
+
"a1-code-block__textarea",
|
|
140
|
+
wrapping && "a1-code-block__textarea--wrapping",
|
|
141
|
+
]
|
|
142
|
+
.filter(Boolean)
|
|
143
|
+
.join(" ")}
|
|
144
|
+
rows={10}
|
|
145
|
+
value={editableValue}
|
|
146
|
+
onChange={handleTextareaChange}
|
|
147
|
+
spellCheck={false}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
) : (
|
|
151
|
+
<pre className="a1-code-block__pre">
|
|
152
|
+
<code className={codeClasses} {...props}>
|
|
153
|
+
{children}
|
|
154
|
+
</code>
|
|
155
|
+
</pre>
|
|
156
|
+
)}
|
|
121
157
|
{copyCode && (
|
|
122
158
|
<Button
|
|
123
159
|
className="a1-code-block__copy"
|
|
@@ -55,6 +55,35 @@
|
|
|
55
55
|
overflow-wrap: anywhere;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
.a1-code-block__textarea {
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
align-self: stretch;
|
|
61
|
+
inline-size: 100%;
|
|
62
|
+
margin: 0;
|
|
63
|
+
padding: var(--base-spacing-16);
|
|
64
|
+
border: var(--component-card-border-width) solid var(--semantic-color-border-subtle);
|
|
65
|
+
border-radius: var(--base-radius-md);
|
|
66
|
+
background: var(--semantic-color-surface-panel);
|
|
67
|
+
font-family: var(--component-inline-font-family-mono);
|
|
68
|
+
font-size: var(--component-inline-code-font-size);
|
|
69
|
+
line-height: 1.6;
|
|
70
|
+
color: var(--semantic-color-text-default);
|
|
71
|
+
white-space: pre;
|
|
72
|
+
overflow-x: auto;
|
|
73
|
+
resize: vertical;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.a1-code-block__textarea:focus-visible {
|
|
77
|
+
outline: none;
|
|
78
|
+
border-color: var(--semantic-color-action-background);
|
|
79
|
+
box-shadow: 0 0 0 var(--component-card-border-width) var(--semantic-color-action-background);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.a1-code-block__textarea--wrapping {
|
|
83
|
+
white-space: pre-wrap;
|
|
84
|
+
overflow-wrap: anywhere;
|
|
85
|
+
}
|
|
86
|
+
|
|
58
87
|
.a1-code-block__copy {
|
|
59
88
|
margin: 0;
|
|
60
89
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ContextMenuItemEntry {
|
|
4
|
+
/** Default entry type — a clickable menu item. */
|
|
5
|
+
type?: 'item';
|
|
6
|
+
/** Unique identifier for this entry. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Display label. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Material Symbols icon name shown before the label. */
|
|
11
|
+
icon?: string;
|
|
12
|
+
/** Keyboard shortcut hint shown after the label (e.g. "⌦", "⌘Z"). */
|
|
13
|
+
shortcut?: string;
|
|
14
|
+
/** Visual variant. `destructive` uses error colors to signal a dangerous action. Default: "default" */
|
|
15
|
+
variant?: 'default' | 'destructive';
|
|
16
|
+
/** Highlights this item as the currently active/selected option. */
|
|
17
|
+
active?: boolean;
|
|
18
|
+
/** Prevents interaction. */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Called when the item is clicked. The menu closes automatically after. */
|
|
21
|
+
onClick?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ContextMenuDividerEntry {
|
|
25
|
+
type: 'divider';
|
|
26
|
+
id: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ContextMenuGroupEntry {
|
|
30
|
+
type: 'group';
|
|
31
|
+
id: string;
|
|
32
|
+
/** Label shown as a non-interactive section heading. */
|
|
33
|
+
label: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ContextMenuEntry =
|
|
37
|
+
| ContextMenuItemEntry
|
|
38
|
+
| ContextMenuDividerEntry
|
|
39
|
+
| ContextMenuGroupEntry;
|
|
40
|
+
|
|
41
|
+
export interface ContextMenuProps {
|
|
42
|
+
/** Controls visibility. Default: false */
|
|
43
|
+
open?: boolean;
|
|
44
|
+
/** Horizontal position in viewport pixels (typically event.clientX). */
|
|
45
|
+
x?: number;
|
|
46
|
+
/** Vertical position in viewport pixels (typically event.clientY). */
|
|
47
|
+
y?: number;
|
|
48
|
+
/** Menu entries — items, dividers, and group headings. */
|
|
49
|
+
items?: ContextMenuEntry[];
|
|
50
|
+
/** Called when the menu should close (outside click or Escape key). */
|
|
51
|
+
onClose?: () => void;
|
|
52
|
+
/** Accessible name for the menu. Default: "Context menu" */
|
|
53
|
+
'aria-label'?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export declare function ContextMenu(props: ContextMenuProps): React.ReactElement | null;
|