@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtivr4/a1-design-system-react",
3
- "version": "0.12.0",
3
+ "version": "0.13.3",
4
4
  "description": "React components for the A1 token-driven design system.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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">
@@ -80,8 +80,6 @@
80
80
  }
81
81
 
82
82
  .a1-blockquote--feature .a1-blockquote__cite {
83
- text-transform: uppercase;
84
- letter-spacing: 0.06em;
85
83
  font-size: var(--semantic-font-size-body-xs);
86
84
  margin-top: var(--base-spacing-20);
87
85
  }
@@ -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={Component === "button" ? type ?? "button" : 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);
@@ -57,8 +57,6 @@
57
57
  color: var(--semantic-color-text-muted);
58
58
  text-align: center;
59
59
  padding-block: var(--component-calendar-cell-padding);
60
- text-transform: uppercase;
61
- letter-spacing: 0.06em;
62
60
  border-block-end: 1px solid var(--semantic-color-border-subtle);
63
61
  }
64
62
 
@@ -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-body-sm);
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-body-md);
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-body-xs);
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 = "default",
16
+ size = "default",
17
17
  columns,
18
- multiple = false,
19
- inlineIcon = false,
20
- required = false,
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 && "a1-choice-group--inline-icon",
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 && "a1-choice-item--disabled",
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.icon && (
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-md);
16
- --a1-cg-subtext-size: var(--semantic-font-size-body-sm);
17
- --a1-cg-legend-size: var(--semantic-font-size-body-sm);
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
- --a1-cg-label-size: var(--semantic-font-size-body-sm);
41
- --a1-cg-subtext-size: var(--semantic-font-size-body-xs);
42
- --a1-cg-legend-size: var(--semantic-font-size-body-xs);
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-body-md);
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);
@@ -138,7 +141,6 @@
138
141
  display: flex;
139
142
  flex-wrap: wrap;
140
143
  gap: var(--a1-cg-gap);
141
- margin-top: var(--a1-cg-items-top-gap);
142
144
  }
143
145
 
144
146
  /* Fixed column count when --a1-cg-columns is set */
@@ -328,6 +330,49 @@
328
330
  flex-shrink: 0;
329
331
  }
330
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
+
331
376
  /* ─── Error state ───────────────────────────────────────────────────────────── */
332
377
 
333
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
- <pre className="a1-code-block__pre">
117
- <code className={codeClasses} {...props}>
118
- {children}
119
- </code>
120
- </pre>
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;