@antadesign/anta 0.3.28 → 0.3.29

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 (135) hide show
  1. package/NOTICES.md +32 -1
  2. package/dist/anta_helpers.d.ts +21 -0
  3. package/dist/anta_helpers.js +24 -0
  4. package/dist/box-observation.d.ts +8 -0
  5. package/dist/box-observation.js +41 -0
  6. package/dist/box-types.d.ts +2 -0
  7. package/dist/bundle.css +1 -1
  8. package/dist/bundle.js +64 -27
  9. package/dist/components/Box.d.ts +28 -12
  10. package/dist/components/Box.js +18 -11
  11. package/dist/components/Breadcrumbs.css +1 -1
  12. package/dist/components/Calendar.js +3 -5
  13. package/dist/components/Checkbox.d.ts +2 -2
  14. package/dist/components/Input.d.ts +8 -2
  15. package/dist/components/Input.js +4 -0
  16. package/dist/components/InputAutocomplete.js +3 -12
  17. package/dist/components/InputDate.css +1 -1
  18. package/dist/components/InputDate.js +2 -0
  19. package/dist/components/Menu.d.ts +4 -4
  20. package/dist/components/Menu.js +1 -1
  21. package/dist/components/Panel.d.ts +17 -0
  22. package/dist/components/Panel.js +22 -0
  23. package/dist/components/RadioGroup.d.ts +2 -1
  24. package/dist/components/Select.d.ts +14 -14
  25. package/dist/components/Select.js +47 -35
  26. package/dist/components/SelectFaceted.d.ts +17 -6
  27. package/dist/components/SelectFaceted.js +61 -41
  28. package/dist/components/Steps.css +1 -1
  29. package/dist/components/Switch.d.ts +3 -3
  30. package/dist/components/TabPanel.d.ts +3 -8
  31. package/dist/components/TabPanel.js +2 -1
  32. package/dist/components/Tabs.d.ts +2 -2
  33. package/dist/components/Tabs.js +2 -1
  34. package/dist/components/Tooltip.d.ts +1 -1
  35. package/dist/components/Tooltip.js +1 -1
  36. package/dist/components/select-parts.css +1 -1
  37. package/dist/elements/a-avatar.css +1 -1
  38. package/dist/elements/a-banner.css +1 -1
  39. package/dist/elements/a-box.css +1 -1
  40. package/dist/elements/a-box.d.ts +6 -6
  41. package/dist/elements/a-box.js +74 -23
  42. package/dist/elements/a-button.css +1 -1
  43. package/dist/elements/a-button.d.ts +9 -1
  44. package/dist/elements/a-button.js +64 -1
  45. package/dist/elements/a-calendar.css +1 -1
  46. package/dist/elements/a-capture.css +1 -1
  47. package/dist/elements/a-card.css +1 -1
  48. package/dist/elements/a-checkbox.css +1 -1
  49. package/dist/elements/a-checkbox.d.ts +3 -0
  50. package/dist/elements/a-checkbox.js +17 -1
  51. package/dist/elements/a-copy.css +1 -1
  52. package/dist/elements/a-dialog.css +1 -1
  53. package/dist/elements/a-expander.css +1 -1
  54. package/dist/elements/a-icon.css +1 -1
  55. package/dist/elements/a-icon.shapes.css +1 -1
  56. package/dist/elements/a-input-time.css +1 -1
  57. package/dist/elements/a-input-time.d.ts +8 -0
  58. package/dist/elements/a-input-time.js +115 -8
  59. package/dist/elements/a-input.css +1 -1
  60. package/dist/elements/a-input.d.ts +27 -1
  61. package/dist/elements/a-input.js +176 -35
  62. package/dist/elements/a-loader.css +1 -1
  63. package/dist/elements/a-menu-group.css +1 -1
  64. package/dist/elements/a-menu-item.css +1 -1
  65. package/dist/elements/a-menu-item.js +1 -0
  66. package/dist/elements/a-menu-separator.css +1 -1
  67. package/dist/elements/a-menu.css +1 -1
  68. package/dist/elements/a-menu.d.ts +8 -13
  69. package/dist/elements/a-menu.js +146 -45
  70. package/dist/elements/a-panel.css +1 -0
  71. package/dist/elements/a-panel.d.ts +16 -0
  72. package/dist/elements/a-panel.js +153 -0
  73. package/dist/elements/a-progress.css +1 -1
  74. package/dist/elements/a-radio-group.css +1 -1
  75. package/dist/elements/a-radio.css +1 -1
  76. package/dist/elements/a-radio.d.ts +4 -0
  77. package/dist/elements/a-radio.js +18 -0
  78. package/dist/elements/a-slider.css +1 -1
  79. package/dist/elements/a-slider.d.ts +1 -0
  80. package/dist/elements/a-slider.js +3 -3
  81. package/dist/elements/a-switch.css +1 -1
  82. package/dist/elements/a-switch.d.ts +3 -0
  83. package/dist/elements/a-switch.js +17 -1
  84. package/dist/elements/a-tab.css +1 -1
  85. package/dist/elements/a-tabpanel.css +1 -1
  86. package/dist/elements/a-tabs.css +1 -1
  87. package/dist/elements/a-tag.css +1 -1
  88. package/dist/elements/a-text.css +1 -1
  89. package/dist/elements/a-title.css +1 -1
  90. package/dist/elements/a-toast.css +1 -1
  91. package/dist/elements/a-toaster.css +1 -1
  92. package/dist/elements/a-tooltip.css +1 -1
  93. package/dist/elements/a-tooltip.js +30 -7
  94. package/dist/elements/focus.d.ts +9 -0
  95. package/dist/elements/focus.js +41 -0
  96. package/dist/elements/index.d.ts +1 -0
  97. package/dist/elements/index.js +3 -0
  98. package/dist/elements/panel-focus.d.ts +10 -0
  99. package/dist/elements/panel-focus.js +86 -0
  100. package/dist/elements/shadow-aria.d.ts +10 -0
  101. package/dist/elements/shadow-aria.js +155 -0
  102. package/dist/general_types.d.ts +48 -19
  103. package/dist/generate-icons.mjs +3 -0
  104. package/dist/index.d.ts +4 -1
  105. package/dist/index.js +2 -0
  106. package/dist/jsx-runtime.d.ts +10 -1
  107. package/dist/reset.css +1 -1
  108. package/dist/theme-antithesis.css +1 -1
  109. package/dist/theme-antune.css +1 -1
  110. package/dist/tokens.css +1 -1
  111. package/docs/accessibility.md +4 -2
  112. package/docs/changelog.md +82 -4
  113. package/docs/colors.md +6 -6
  114. package/docs/components/box.md +98 -34
  115. package/docs/components/breadcrumbs.md +33 -0
  116. package/docs/components/button.md +2 -2
  117. package/docs/components/capture.md +8 -8
  118. package/docs/components/checkbox.md +9 -4
  119. package/docs/components/input-autocomplete.md +6 -0
  120. package/docs/components/input-date.md +8 -2
  121. package/docs/components/input.md +99 -40
  122. package/docs/components/menu.md +3 -3
  123. package/docs/components/panel.md +189 -0
  124. package/docs/components/radio.md +4 -2
  125. package/docs/components/select-faceted.md +57 -2
  126. package/docs/components/select.md +63 -51
  127. package/docs/components/steps.md +4 -4
  128. package/docs/components/switch.md +12 -6
  129. package/docs/components/tabs.md +21 -8
  130. package/docs/components/tooltip.md +6 -4
  131. package/docs/index.md +1 -0
  132. package/docs/install-config.md +5 -4
  133. package/docs/normalization.md +3 -1
  134. package/docs/theming.md +1 -1
  135. package/package.json +4 -1
@@ -23,6 +23,12 @@ the first suggestion (then ↑/↓ move through them), and
23
23
  Enter on a highlighted row picks it. Esc or an outside click
24
24
  closes the list; clicking the field to place the caret keeps it open.
25
25
 
26
+ The focused field is associated directly with the suggestion popup and its
27
+ active option through ARIA element reflection. These relationships do not
28
+ require generated popup or option IDs, so separate application roots cannot
29
+ collide. Explicit ARIA relationships authored on a raw Anta Input take
30
+ precedence.
31
+
26
32
  ## Filtering
27
33
 
28
34
  `filter` decides how suggestions match the text:
@@ -212,9 +212,15 @@ the standard constraint-validation pipeline.
212
212
 
213
213
  ## Accessibility
214
214
 
215
+ The focused Input field is associated directly with its Calendar dialog through
216
+ ARIA element reflection. The relationship does not require a generated popup ID,
217
+ so separate application roots can render InputDate instances without ID
218
+ collisions. An explicitly authored relationship on a raw Anta Input takes
219
+ precedence.
220
+
215
221
  The calendar is a labelled **group** of day buttons, a flat grid rather than a
216
- nested `grid`/`row`/`gridcell` tree. The group is named by the visible month
217
- (`aria-labelledby`). Each day is a `<button>` whose `aria-label` is its full date
222
+ nested `grid`/`row`/`gridcell` tree. The visible month names the group directly.
223
+ Each day is a `<button>` whose `aria-label` is its full date
218
224
  including weekday ("Sunday, June 28, 2026"), with `aria-pressed` on the selected day
219
225
  and `aria-current="date"` on today. The decorative weekday headers are `aria-hidden`,
220
226
  since each day already names its weekday. A roving `tabindex` keeps a single tab
@@ -9,9 +9,9 @@ for styling through `::part(input)`.
9
9
  ## Size
10
10
 
11
11
  ```tsx
12
- <Input size="small" label="Small" placeholder="24px tall" />
13
- <Input size="medium" label="Medium" placeholder="28px tall" /> {/* default */}
14
- <Input size="large" label="Large" placeholder="32px tall" />
12
+ <Input size="small" label="Small" placeholder="24px tall" autoComplete="off" />
13
+ <Input size="medium" label="Medium" placeholder="28px tall" autoComplete="off" /> {/* default */}
14
+ <Input size="large" label="Large" placeholder="32px tall" autoComplete="off" />
15
15
  ```
16
16
 
17
17
  Three sizes (`medium` is the default). The field **height**, the **type scale**
@@ -27,8 +27,8 @@ Anta's text scale, so a field lines up with same-size `Text` / `Button`.
27
27
  ## Label and hint
28
28
 
29
29
  ```tsx
30
- <Input label="Display name" defaultValue="Ada Lovelace" />
31
- <Input label="API key" hint="Find this in Settings → Developers." placeholder="sk-…" />
30
+ <Input label="Display name" defaultValue="Ada Lovelace" autoComplete="name" />
31
+ <Input label="API key" hint="Find this in Settings → Developers." placeholder="sk-…" autoComplete="off" />
32
32
  ```
33
33
 
34
34
  `label` sits above the field; `hint` is neutral helper text below it
@@ -40,12 +40,18 @@ mirrors the label text into `aria-label`, since a native `<label for>` can't
40
40
  cross the shadow boundary. The `hint` (or error) is mirrored the same way into
41
41
  `aria-description`, so a screen reader announces the message too.
42
42
 
43
+ Standard `role` and `aria-*` attributes on `<Input>` or raw `<a-input>` are
44
+ applied to the native input, textarea, or button inside its shadow root. This
45
+ keeps the focused native control as the single accessible control. If you query
46
+ the DOM directly, read those semantics from `a-input.control` rather than from
47
+ the custom-element host.
48
+
43
49
  ## Tooltip
44
50
 
45
51
  ```tsx
46
52
  {/* Drop a <Tooltip> in as a child — it attaches to the field and shows on hover,
47
53
  exactly like a tooltip on any other element. */}
48
- <Input label="API key" placeholder="sk-…">
54
+ <Input label="API key" placeholder="sk-…" autoComplete="off">
49
55
  <Tooltip>Find this in Settings → Developers.</Tooltip>
50
56
  </Input>
51
57
  ```
@@ -60,12 +66,12 @@ content.)
60
66
 
61
67
  ```tsx
62
68
  {/* No status = neutral helper text */}
63
- <Input label="Display name" defaultValue="Ada Lovelace" hint="This is your public name." />
64
- <Input label="Workspace" defaultValue="acme" status="info" hint="Lowercase letters and dashes only." />
65
- <Input label="Username" defaultValue="ada" status="success" hint="Username is available." />
66
- <Input label="Password" type="password" defaultValue="hunter2" status="warning" hint="Weak — add more characters." />
67
- <Input label="Email" defaultValue="not-an-email" status="critical" hint="Enter a valid email address." />
68
- <Input label="Plan" defaultValue="Pro" status="brand" hint="You're on the Pro plan." />
69
+ <Input label="Display name" defaultValue="Ada Lovelace" hint="This is your public name." autoComplete="name" />
70
+ <Input label="Workspace" defaultValue="acme" status="info" hint="Lowercase letters and dashes only." autoComplete="off" />
71
+ <Input label="Username" defaultValue="ada" status="success" hint="Username is available." autoComplete="username" />
72
+ <Input label="Password" type="password" defaultValue="hunter2" status="warning" hint="Weak — add more characters." autoComplete="new-password" />
73
+ <Input label="Email" type="email" defaultValue="not-an-email" status="critical" hint="Enter a valid email address." autoComplete="email" />
74
+ <Input label="Plan" defaultValue="Pro" status="brand" hint="You're on the Pro plan." autoComplete="off" />
69
75
  ```
70
76
 
71
77
  `status` tints the border and the `hint`, and prefixes a glyph — `critical`,
@@ -92,15 +98,16 @@ with `statusIcon={false}`.
92
98
  ```tsx
93
99
  {/* clearable — the clear button shows once there's a value */}
94
100
  {/* dimActions — adornments rest quiet, brighten when the field is hovered/focused */}
95
- <Input label="Search" placeholder="Search…" clearable dimActions defaultValue="design tokens" />
101
+ <Input label="Search" placeholder="Search…" clearable dimActions defaultValue="design tokens" autoComplete="off" />
96
102
 
97
103
  {/* type="search" is a shorthand for the leading search icon + clearable below */}
98
- <Input label="Search" type="search" placeholder="Search…" />
104
+ <Input label="Search" type="search" placeholder="Search…" autoComplete="off" />
99
105
 
100
106
  {/* leading / trailing take any node — see the live playground above */}
101
- <Input label="Search" leading={<Icon shape="search" />} clearable dimActions />
107
+ <Input label="Search" leading={<Icon shape="search" />} clearable dimActions autoComplete="off" />
102
108
  <Input
103
109
  label="Date"
110
+ autoComplete="off"
104
111
  leading={<Icon shape="calendar" />}
105
112
  trailing={<Button priority="quaternary" size="small" icon="eye" aria-label="Pick" />}
106
113
  dimActions
@@ -149,6 +156,7 @@ const [reveal, setReveal] = useState(false)
149
156
 
150
157
  <Input
151
158
  label="Password"
159
+ autoComplete="current-password"
152
160
  type={reveal ? 'text' : 'password'}
153
161
  defaultValue="hunter2"
154
162
  dimActions
@@ -176,10 +184,10 @@ Firefox. So Anta keeps the native field.
176
184
 
177
185
  ```tsx
178
186
  {/* Grows with content, capped at 6 rows, then scrolls */}
179
- <Input multiline maxRows={6} label="Bio (autogrows)" placeholder="Tell us…" />
187
+ <Input multiline maxRows={6} label="Bio (autogrows)" placeholder="Tell us…" autoComplete="off" />
180
188
 
181
189
  {/* Constant height of 3 rows */}
182
- <Input multiline rows={3} label="Notes (fixed 3 rows)" placeholder="…" />
190
+ <Input multiline rows={3} label="Notes (fixed 3 rows)" placeholder="…" autoComplete="off" />
183
191
  ```
184
192
 
185
193
  `multiline` renders a `<textarea>` under the same API. With **no `rows`** it
@@ -195,11 +203,11 @@ browser.
195
203
 
196
204
  ```tsx
197
205
  // Uncontrolled: the element updates its value after input.
198
- <Input label="Name" defaultValue="Ada" onChange={(e) => log(e.target.value)} />
206
+ <Input label="Name" defaultValue="Ada" autoComplete="name" onChange={(e) => log(e.target.value)} />
199
207
 
200
208
  // Controlled: application state supplies the value.
201
209
  const [v, setV] = useState('')
202
- <Input label="Name" value={v} onInput={(e) => setV(e.target.value)} />
210
+ <Input label="Name" value={v} autoComplete="name" onInput={(e) => setV(e.target.value)} />
203
211
  ```
204
212
 
205
213
  Like a native input: pass `defaultValue` for **uncontrolled**, or `value` +
@@ -240,7 +248,7 @@ value-based, also has only `onValueChange`.)
240
248
 
241
249
  ```tsx
242
250
  <form onSubmit={handle}>
243
- <Input name="email" label="Email" type="email" required clearable />
251
+ <Input name="email" label="Email" type="email" autoComplete="email" required clearable />
244
252
  <Button tone="brand" type="submit" label="Sign up" />
245
253
  <Button priority="tertiary" type="reset" label="Reset" />
246
254
  </form>
@@ -254,12 +262,12 @@ restores the field to its `defaultValue` via `formResetCallback`.
254
262
  ## Types and validation
255
263
 
256
264
  ```tsx
257
- <Input type="text" label="Text" />
258
- <Input type="email" label="Email" />
259
- <Input type="password" label="Password" />
260
- <Input type="tel" label="Tel" />
261
- <Input type="url" label="URL" />
262
- <Input type="number" label="Number" min={0} max={100} />
265
+ <Input type="text" label="Text" autoComplete="off" />
266
+ <Input type="email" label="Email" autoComplete="email" />
267
+ <Input type="password" label="Password" autoComplete="current-password" />
268
+ <Input type="tel" label="Tel" autoComplete="tel" />
269
+ <Input type="url" label="URL" autoComplete="url" />
270
+ <Input type="number" label="Number" autoComplete="off" min={0} max={100} />
263
271
  ```
264
272
 
265
273
  `type` accepts `text` (default), `email`, `password`, `tel`, `url`, and
@@ -267,6 +275,12 @@ restores the field to its `defaultValue` via `formResetCallback`.
267
275
  constraint validation. (`search` is omitted on purpose — see above.)
268
276
  `number` also takes `min` / `max` / `step`.
269
277
 
278
+ Set `autoComplete` to describe the field's purpose. `email`, `tel`, and `url`
279
+ derive their matching token from `type`. Other purposes need an explicit token,
280
+ such as `name`, `username`, `current-password`, `new-password`, or
281
+ `one-time-code`. Use `autoComplete="off"` for examples and application fields
282
+ that do not represent autofill data.
283
+
270
284
  For a **controlled** numeric field — one whose `value` you hold in state — prefer
271
285
  `type="text"` with `inputMode="decimal"` (or `"numeric"`) over `type="number"`. A
272
286
  number input runs value sanitization, so a mid-typing `"1."` or `"1.0"` reports an
@@ -296,10 +310,10 @@ function onSubmit(e) {
296
310
  }
297
311
 
298
312
  <form noValidate onSubmit={onSubmit}>
299
- <Input name="email" type="email" required hint={errors.email}
313
+ <Input name="email" type="email" autoComplete="email" required hint={errors.email}
300
314
  status={errors.email ? 'critical' : undefined}
301
315
  onInput={() => setErrors(p => ({ ...p, email: undefined }))} />
302
- <Input name="age" type="number" min="18" max="120" hint={errors.age}
316
+ <Input name="age" type="number" autoComplete="off" min="18" max="120" hint={errors.age}
303
317
  status={errors.age ? 'critical' : undefined}
304
318
  onInput={() => setErrors(p => ({ ...p, age: undefined }))} />
305
319
  <Button type="submit" label="Submit" />
@@ -345,7 +359,7 @@ Re-template the host to two columns and drop each part into one. The hint stays
345
359
  under the field by landing in the same column.
346
360
 
347
361
  ```tsx
348
- <Input className="label-side" label="Display name" hint="Shown on your profile" />
362
+ <Input className="label-side" label="Display name" hint="Shown on your profile" autoComplete="name" />
349
363
 
350
364
  {/* .label-side is just for the demo — use your own selector */}
351
365
  <style>{`
@@ -369,9 +383,9 @@ left-edge lines up. Each hint still tucks under its own field.
369
383
 
370
384
  ```tsx
371
385
  <div className="aligned-form">
372
- <Input label="Email" type="email" hint="We'll never share it." />
373
- <Input label="Confirm password" type="password" />
374
- <Input label="PIN" />
386
+ <Input label="Email" type="email" autoComplete="email" hint="We'll never share it." />
387
+ <Input label="Confirm password" type="password" autoComplete="new-password" />
388
+ <Input label="PIN" autoComplete="one-time-code" />
375
389
  </div>
376
390
 
377
391
  <style>{`
@@ -391,7 +405,7 @@ unit. Two columns (field, then hint); the label spans the top, and the hint
391
405
  centers against the field.
392
406
 
393
407
  ```tsx
394
- <Input className="hint-right" label="Username" hint="3–20 characters" placeholder="ada" />
408
+ <Input className="hint-right" label="Username" hint="3–20 characters" placeholder="ada" autoComplete="username" />
395
409
 
396
410
  <style>{`
397
411
  a-input.hint-right {
@@ -412,10 +426,10 @@ wraps to fewer columns as it narrows. Resize the preview to see it reflow.
412
426
 
413
427
  ```tsx
414
428
  <div className="wrap-fields">
415
- <Input label="First name" />
416
- <Input label="Last name" />
417
- <Input label="Email" type="email" />
418
- <Input label="Phone" type="tel" />
429
+ <Input label="First name" autoComplete="given-name" />
430
+ <Input label="Last name" autoComplete="family-name" />
431
+ <Input label="Email" type="email" autoComplete="email" />
432
+ <Input label="Phone" type="tel" autoComplete="tel" />
419
433
  </div>
420
434
 
421
435
  <style>{`
@@ -430,6 +444,7 @@ wraps to fewer columns as it narrows. Resize the preview to see it reflow.
430
444
  |------|------|---------|-------------|
431
445
  | `autoComplete?` | 'off' \| 'on' \| 'name' \| 'username' \| 'email' \| 'current-password' \| 'new-password' \| 'one-time-code' \| 'tel' \| 'url' \| (string & {}) | — | Native autocomplete token. Overrides the value derived from `type` (`email` / `tel` / `url`) — set it for the cases `type` can't express, e.g. `username`, `current-password`, `new-password`, `one-time-code`, or `off`. |
432
446
  | `autoFocus?` | boolean | — | Focus this field when its containing `Dialog` opens. |
447
+ | `button?` | boolean | — | Render the field surface with a native button as its focus target. This is available for Input-styled popup triggers; the value becomes the button text and the placeholder is shown while it is empty. |
433
448
  | `children?` | ReactNode | — | Extra content rendered directly under the field, above the hint/error (it pushes the message down). A no-box child like an Anta `<Tooltip>` takes no space and anchors to the field — consistent with how tooltips attach to any other element. Use the named `leading` / `trailing` props for in-field content. |
434
449
  | `clearable?` | boolean | — | Show a clear button as the first trailing item once the field has a value. |
435
450
  | `defaultValue?` | string | — | Initial value for the uncontrolled case. |
@@ -457,7 +472,7 @@ wraps to fewer columns as it narrows. Resize the preview to see it reflow.
457
472
  | `placeholder?` | string | — | Placeholder shown when empty. |
458
473
  | `readOnly?` | boolean | — | Make the field read-only. |
459
474
  | `required?` | boolean | — | Mark the field required (drives native validity). |
460
- | `role?` | string | — | ARIA `role` for the field — e.g. `combobox` when the input drives a suggestion `listbox` (see `InputAutocomplete`). Left unset by default. |
475
+ | `role?` | string | — | ARIA `role` for the field — e.g. `combobox` when the input drives a suggestion `listbox` (see `InputAutocomplete`). The custom element delegates it, together with standard `aria-*` props, to the focused native shadow control. Left unset by default. |
461
476
  | `round?` | boolean \| number \| string | — | Fully-round the field (`border-radius: 999px`). Pass a `number` (px) or a CSS length string for a custom radius. The `clearable` × button always rounds to a circle to match (it isn't sized by a custom field value). |
462
477
  | `rows?` | number | — | Fixed visible row count — a constant-height `<textarea>` (implies `multiline`). |
463
478
  | `size?` | 'small' \| 'medium' \| 'large' | medium | Size variant. small=24px, medium=28px, large=32px tall; the type scale and icon track the size (small 13/16 + 14px icon · medium 15/20 + 16px · large 17/22 + 18px). |
@@ -478,7 +493,7 @@ Use the web component directly when you are not using React or Preact and a nati
478
493
  Slots hold the label, hint, and controls around the native field.
479
494
 
480
495
  ```html
481
- <a-input name="email" placeholder="you@example.com">
496
+ <a-input name="email" type="email" autocomplete="email" placeholder="you@example.com">
482
497
  <span slot="label">Email</span>
483
498
  <span slot="hint">We only use it for account notices.</span>
484
499
  </a-input>
@@ -524,7 +539,7 @@ Reach for the props first: **`status`** sets a validation tone (border + message
524
539
  dimensions. The focus ring is the global [`--focus-ring`](../colors.md#focus-ring).
525
540
 
526
541
  ```tsx
527
- <Input tone="#e0457b" label="Custom accent" />
542
+ <Input tone="#e0457b" label="Custom accent" autoComplete="off" />
528
543
  ```
529
544
 
530
545
  For everything else, `<a-input>` is shadow-DOM — style its **parts** with plain
@@ -563,5 +578,49 @@ a-input.fancy::part(field) {
563
578
  padding-inline: 20px; /* roomier */
564
579
  }
565
580
  a-input.fancy::part(input) { color: light-dark(#ffffff, #1a1a1a); }
581
+ a-input.fancy::part(input)::placeholder {
582
+ color: light-dark(#b8b8b8, #666666);
583
+ opacity: 1;
584
+ }
566
585
  a-input.fancy:focus-within::part(field) { background: light-dark(#161616, #ffffff); }
567
586
  ```
587
+
588
+ ### Placeholder contrast
589
+
590
+ If the label, marking, context, or hint leaves the field's purpose unclear,
591
+ a stronger placeholder can help. Keep an accessible name and put essential
592
+ instructions in a persistent label or hint.
593
+
594
+ Antune uses `--text-4` for placeholders. These ratios compare the default neutral
595
+ Input backgrounds with the example's literal color:
596
+
597
+ | Style | Placeholder | Background | Contrast |
598
+ | --- | --- | --- | --- |
599
+ | Antune light theme | `#7a737c` | `#ffffff` | 4.59:1 |
600
+ | Antune dark theme | `#7c737c` | `#000000` | 4.60:1 |
601
+ | Example | `#767676` | `#ffffff` | 4.54:1 |
602
+
603
+ `#767676` is the lightest opaque 8-bit neutral gray that passes 4.5:1 on white.
604
+ W3C recommends [sufficient placeholder contrast](https://www.w3.org/WAI/tutorials/forms/instructions/#styling).
605
+ Redundant placeholders remain debated in the
606
+ [meeting discussion](https://www.w3.org/2025/04/16-matf-minutes.html) and
607
+ [open W3C issue](https://github.com/w3c/wcag/issues/4343); there is no agreed exemption.
608
+
609
+ This demo uses `#767676` on white in every state and theme.
610
+ Replace `.placeholder-contrast` with your own class.
611
+
612
+ ```tsx
613
+ <Input className="placeholder-contrast" label="Email" type="email" autoComplete="email" placeholder="you@example.com" />
614
+ ```
615
+
616
+ ```css
617
+ a-input.placeholder-contrast::part(field) { background: #ffffff; }
618
+ a-input.placeholder-contrast::part(input) { color: #161616; }
619
+ a-input.placeholder-contrast::part(input)::placeholder {
620
+ color: #767676;
621
+ opacity: 1;
622
+ }
623
+ ```
624
+
625
+ Check contrast separately if you use a different background.
626
+ Native inputs and textareas use `::placeholder` without `::part(input)`.
@@ -415,7 +415,7 @@ navigating links.
415
415
  | `selectionIndicator?` | 'checkbox' \| 'radio' \| 'check' | — | Turn the row into a checkable item, driven by `selected` (the row stays the control and carries `aria-checked`): - `'checkbox'` → `role="menuitemcheckbox"`, a leading passive `<a-checkbox>` (before `icon`); the tint is dropped (the box carries state). - `'radio'` → `role="menuitemradio"`, a leading passive `<a-radio>`; tint dropped. - `'check'` → `role="menuitemradio"`, a trailing check glyph on the selected row *and* the background tint (the canonical single-select look). Omit for a plain row (the default). |
416
416
  | `submenu?` | boolean | — | Marks this item as a submenu parent: adds the trailing chevron and `aria-haspopup="menu"`. Nest the flyout as a `<Menu>` child. |
417
417
  | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | neutral | Semantic tone — colors the label, icon, and hover/selected tint (and the `checkbox`/`radio` indicator, which adopts it). A named tone, or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`) for a one-off custom tone whose hue + chroma are kept while the lightness is pinned to match the brand text. `critical` is the destructive action; `neutral` (the default) is the standard gray. |
418
- | `toneScope?` | ToneScope | 'all' | Apply `tone` to every row state, or only while the row is selected. In `selected` scope, an unselected row and its checkbox/radio indicator stay neutral. |
418
+ | `toneScope?` | 'all' \| 'selected' | 'all' | Apply `tone` to every row state, or only while the row is selected. In `selected` scope, an unselected row and its checkbox/radio indicator stay neutral. |
419
419
  | `value?` | string \| number | — | An opaque value identifying this item, handed back in `onSelect`'s detail so a shared handler can tell which row was chosen without a per-item closure. |
420
420
 
421
421
  ## Keyboard
@@ -461,7 +461,7 @@ or do nothing to reject. (Submenus are always uncontrolled, regardless of `open`
461
461
  | `inset?` | number \| string | 4 | Inner inset for the scroll body and named header/footer content. A number is pixels; a CSS length string is used as written. |
462
462
  | `nohover?` | boolean | — | Submenu-only (a `<Menu>` nested inside a `MenuItem`); ignored on a root menu. Submenus open on hover by default (with intent timing) as well as on click — set `nohover` to make this submenu click-only. Hover-intent is mouse-only regardless: on touch (and pen) a submenu always opens on tap and stays open until dismissed. |
463
463
  | `offset?` | number | 4 | Gap in pixels between the trigger and the menu. |
464
- | `onactivedescendant?` | (e) => void | — | Combobox-mode cursor report (raw element event). Fires when the active option changes as arrow keys move the cursor while focus stays in a `[data-menu-search]` filter field; `detail.id` is the active option's `id` (`null` when none). The reactive layer that renders the field reflects this onto its `aria-activedescendant` — the element can't write that light-DOM attribute itself. Passed straight through to the element. |
464
+ | `onactivedescendant?` | (e) => void | — | Combobox-mode cursor report (raw element event). Fires when the active option changes as arrow keys move the cursor while focus stays in a `[data-menu-search]` filter field; `detail.id` is an authored option `id`, or `null`. Built-in Anta Inputs receive the active option through a direct ARIA element relationship; this remains available to custom fields and observers. Passed straight through to the element. |
465
465
  | `onStateChange?` | (event, detail) => void | — | Fired before the open state changes — on open, and on every dismiss (Esc, outside-click, scroll, selecting an item). `event` is the cancelable `statechange`; `detail.next`/`detail.prev` are the requested/previous open state (booleans). It's the declarative way to observe a menu, and the handler you pair with `open` to drive a controlled menu (apply `detail.next` to `open`). Uncontrolled, `event.preventDefault()` vetoes the transition (e.g. keep the menu open). |
466
466
  | `open?` | boolean | — | Controlled open state. Omit for the default **uncontrolled** menu (it opens/closes itself via its triggers). Pass a boolean to **control** it: the menu's visibility follows `open`, and user dismiss (Esc, outside-click, select) fires `onStateChange` *without* self-closing — you update `open` in response. Submenus are always uncontrolled regardless of this. See STATEFUL-COMPONENTS.md. |
467
467
  | `placement?` | 'bottom-start' \| 'bottom-end' \| 'top-start' \| 'top-end' \| 'bottom' \| 'top' \| 'right-start' \| 'right-end' \| 'left-start' \| 'left-end' \| 'right' \| 'left' | bottom-start | Preferred placement relative to the trigger. `bottom` / `top` place the menu below / above; `right` / `left` place it beside (like a submenu flyout). The cross-axis suffix (`-start` / `-end`) aligns the near / far edges, and no suffix centers on the trigger. The menu flips to the opposite side when the preferred one lacks room, and clamps to the viewport. |
@@ -494,7 +494,7 @@ or do nothing to reject. (Submenus are always uncontrolled, regardless of `open`
494
494
  | `submenu?` | boolean | — | Marks this item as a submenu parent: adds the trailing chevron and `aria-haspopup="menu"`. Nest the flyout as a `<Menu>` child. |
495
495
  | `target?` | string | — | Anchor target, e.g. `'_blank'`. |
496
496
  | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | neutral | Semantic tone — colors the label, icon, and hover/selected tint (and the `checkbox`/`radio` indicator, which adopts it). A named tone, or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`) for a one-off custom tone whose hue + chroma are kept while the lightness is pinned to match the brand text. `critical` is the destructive action; `neutral` (the default) is the standard gray. |
497
- | `toneScope?` | ToneScope | 'all' | Apply `tone` to every row state, or only while the row is selected. In `selected` scope, an unselected row and its checkbox/radio indicator stay neutral. |
497
+ | `toneScope?` | 'all' \| 'selected' | 'all' | Apply `tone` to every row state, or only while the row is selected. In `selected` scope, an unselected row and its checkbox/radio indicator stay neutral. |
498
498
  | `value?` | string \| number | — | An opaque value identifying this item, handed back in `onSelect`'s detail so a shared handler can tell which row was chosen without a per-item closure. |
499
499
 
500
500
  ## Web Component
@@ -0,0 +1,189 @@
1
+ # Panel
2
+
3
+ `Panel` is a container that can maximize to the browser viewport without moving
4
+ or remounting its children. It fills its parent's width and height and uses
5
+ `overflow: scroll` by default.
6
+
7
+ Give the parent a defined height for Panel to fill vertically. With an
8
+ auto-height parent, percentage height follows normal CSS sizing and the Panel
9
+ can grow with its content. A parent's `min-height` alone does not generally
10
+ establish a height for `height: 100%` to fill.
11
+
12
+ > **Warning:** For now, Panel is a nonvisual utility container only. Supply your
13
+ > own appearance and controls. Built-in chrome, resizing, and folding are not
14
+ > available yet.
15
+
16
+ ## Usage
17
+
18
+ ### Declarative
19
+
20
+ Panel manages its own state. Use `data-custom-event="paneltoggle"` on
21
+ an Anta button to toggle maximization without a click handler.
22
+
23
+ ```tsx title="Declarative Panel"
24
+ import { Box, Button, Input, Panel } from '@antadesign/anta'
25
+
26
+ function Workspace() {
27
+ return (
28
+ <Panel style={{ background: 'var(--bg-3)' }}>
29
+ <Box
30
+ display="flex"
31
+ gap={16}
32
+ padding={16}
33
+ >
34
+ <Input aria-label="Workspace note" placeholder="Write a note" />
35
+ <Button data-custom-event="paneltoggle">
36
+ Maximize / restore
37
+ </Button>
38
+ </Box>
39
+ </Panel>
40
+ )
41
+ }
42
+ ```
43
+
44
+ ### Controlled
45
+
46
+ Pass `maximized` to manage state in your application. The button updates it
47
+ through `onClick`; `onStateChange` accepts requests from declarative controls
48
+ inside the panel.
49
+
50
+ ```tsx title="Controlled Panel"
51
+ import { useState } from 'react'
52
+ import { Box, Button, Input, Panel } from '@antadesign/anta'
53
+
54
+ function Workspace() {
55
+ const [maximized, setMaximized] = useState(false)
56
+
57
+ return (
58
+ <Panel
59
+ style={{ background: 'var(--bg-3)' }}
60
+ maximized={maximized}
61
+ onStateChange={(_event, { next }) => setMaximized(next)}
62
+ >
63
+ <Box
64
+ display="flex"
65
+ gap={16}
66
+ padding={16}
67
+ >
68
+ <Input aria-label="Workspace note" placeholder="Write a note" />
69
+ <Button onClick={() => setMaximized(value => !value)}>
70
+ {maximized ? 'Restore panel' : 'Maximize panel'}
71
+ </Button>
72
+ </Box>
73
+ </Panel>
74
+ )
75
+ }
76
+ ```
77
+
78
+ ## How does this work?
79
+
80
+ Maximizing applies `popover="manual"` to the internal content slot and calls
81
+ `showPopover()` to fill the viewport. Children keep their DOM ancestry, theme,
82
+ and event bubbling. A placeholder preserves the panel's normal layout size.
83
+ Restoring calls `hidePopover()` and returns the same content to normal layout.
84
+
85
+ ```html title="Internal content while maximized"
86
+ <!-- Managed by Panel inside its shadow root. -->
87
+ <slot part="content" popover="manual" tabindex="-1"></slot>
88
+ ```
89
+
90
+ While maximized, Panel traps focus inside its content. Tab and Shift+Tab cycle
91
+ through its controls. Restoring releases the trap and returns focus to the
92
+ previous external control, when available.
93
+
94
+ Provide a keyboard-accessible **Restore** control: Escape and outside clicks
95
+ do not restore the panel. Panel adds no dialog role, background inertness, or
96
+ scroll lock. Use [Dialog](./dialog.md) for modal interactions.
97
+
98
+ Panel fills the browser viewport, without invoking the Fullscreen API.
99
+ Browsers without Popover API support keep it in normal layout.
100
+
101
+ ## Component props
102
+
103
+ For uncontrolled state, omit `maximized` and optionally set `defaultMaximized`.
104
+ Call `event.preventDefault()` synchronously in `onStateChange` to cancel an
105
+ uncontrolled transition. In controlled mode, reject a request by leaving
106
+ `maximized` unchanged.
107
+
108
+ | Prop | Type | Default | Description |
109
+ |------|------|---------|-------------|
110
+ | `defaultMaximized?` | boolean | — | Initial viewport maximization for an uncontrolled panel, read once on connect. |
111
+ | `maximized?` | boolean | — | Fill the browser viewport and contain keyboard focus within the panel. When supplied, the application owns the state. |
112
+ | `onStateChange?` | (event, detail) => void | — | Requested maximization change. Update `maximized` to accept a controlled request, or prevent the event to veto an uncontrolled request. |
113
+
114
+ ## Web component
115
+
116
+ Import `@antadesign/anta/elements` to register the elements in this example.
117
+ The button toggles the nearest panel without JavaScript handlers.
118
+
119
+ ```html title="Declarative web component"
120
+ <a-panel style="background: var(--bg-3)">
121
+ <a-box display="flex" padding="16px" gap="16px" style="--box-padding: 16px; --box-gap: 16px">
122
+ <a-input aria-label="Workspace note" placeholder="Write a note"></a-input>
123
+ <a-button role="button" tabindex="0" data-custom-event="paneltoggle">
124
+ Maximize / restore
125
+ </a-button>
126
+ </a-box>
127
+ </a-panel>
128
+ ```
129
+
130
+ Use `state="normal"` or `state="maximized"` for controlled state, or `default-state="maximized"`
131
+ for an initially maximized, uncontrolled panel.
132
+
133
+ `requestMaximize()` and `requestRestore()` emit a cancelable, non-bubbling
134
+ `statechange` with `{ next, prev }` using those state strings.
135
+ Anta buttons can send `panelmaximizerequest`, `panelrestorerequest`, or
136
+ `paneltoggle` through `data-custom-event` to the nearest Panel.
137
+
138
+ ## Styling
139
+
140
+ Panel defaults to `display: block`, `width: 100%`, `height: 100%`,
141
+ `box-sizing: border-box`, and `overflow: scroll`. Padding and borders stay inside
142
+ its assigned size. It does not set `flex-grow`.
143
+
144
+ Override these through ordinary CSS. Use `height: auto` for content-driven
145
+ height or `overflow: auto` for scrollbars only when content overflows.
146
+ With `overflow: scroll`, scrollbar visibility and reserved space follow the
147
+ browser and operating system's scrollbar settings.
148
+
149
+ Set the background on Panel. Its maximized surface inherits that background,
150
+ so inner content can keep its natural height. Style light-DOM children, such as
151
+ [Box](./box.md) or [Capture](./capture.md), for content layout shared between both states.
152
+ Use `:state(maximized)` for viewport styling and `::part(content)` for the
153
+ maximized scrolling surface. In normal mode, the content slot uses `display: contents`.
154
+ The maximized surface also uses `overflow: scroll`; override
155
+ `::part(content)` to change its overflow separately from the normal Panel.
156
+
157
+ Enter a note, then select **Maximize panel** and **Restore panel**. The input
158
+ keeps its value. The `panel-demo` classes supply this example's appearance;
159
+ replace them with your own selectors.
160
+
161
+ ```html title="A persistent workspace panel"
162
+ <a-panel class="panel-demo">
163
+ <a-box class="panel-demo-content">
164
+ <a-box class="panel-demo-toolbar" display="flex">
165
+ <a-text weight="medium">Workspace preview</a-text>
166
+ <a-button role="button" tabindex="0" data-custom-event="paneltoggle">
167
+ <span class="panel-demo-maximize">Maximize panel</span>
168
+ <span class="panel-demo-restore">Restore panel</span>
169
+ </a-button>
170
+ </a-box>
171
+ <a-capture>
172
+ <a-input placeholder="Write a note before maximizing" aria-label="Workspace note"></a-input>
173
+ <a-text>Panel keeps this Box and Capture in their original DOM location.</a-text>
174
+ </a-capture>
175
+ </a-box>
176
+ </a-panel>
177
+ ```
178
+
179
+ ```css title="Preview layout and appearance"
180
+ .panel-demo { border: 1px solid var(--border-4); border-radius: 8px; background: var(--bg-3); }
181
+ .panel-demo-content { display: flex; flex-direction: column; gap: 20px; padding: 20px; }
182
+ .panel-demo-toolbar { justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
183
+ .panel-demo-content a-capture { display: grid; gap: 16px; }
184
+ .panel-demo-content a-input { width: 100%; }
185
+ .panel-demo-restore { display: none; }
186
+ .panel-demo:state(maximized) .panel-demo-content { padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); }
187
+ .panel-demo:state(maximized) .panel-demo-maximize { display: none; }
188
+ .panel-demo:state(maximized) .panel-demo-restore { display: inline; }
189
+ ```
@@ -165,7 +165,9 @@ Under the hood the split is deliberate. The `<a-radio-group>` web component
165
165
  **never mutates the DOM** — it coordinates entirely off-DOM: selection is set as a
166
166
  property on each `<a-radio>` (which reflects it into `:state(selected)` +
167
167
  `aria-checked` via its own `ElementInternals`), the form value goes through
168
- `ElementInternals`, and focus is tracked with `aria-activedescendant`. The
168
+ `ElementInternals`, and focus is tracked with `aria-activedescendant`. Each
169
+ option's light-DOM label and hint also become its accessible name and description
170
+ through direct element references, without generated IDs. The
169
171
  **roving `tabindex`** — which option Tab lands on — is the one piece rendered
170
172
  declaratively, by the `RadioGroup` wrapper, from the current value. That's why
171
173
  selection is owned by the element but the wrapper holds the value: it needs it to
@@ -270,7 +272,7 @@ element — `Input`, and the composed `Select` — skip it and expose only
270
272
  | `size?` | 'small' \| 'medium' \| 'large' | 'medium' | Size applied to every option (an option's own `size` wins). |
271
273
  | `status?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' | 'neutral' | Validation/feedback tone for the group `hint` — recolors it (same tone set as `Input`'s `status`). Use `critical` for an error message, etc.; omit for the neutral default. |
272
274
  | `tone?` | 'brand' \| 'neutral' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Mark tone applied to every option (an option's own `tone` wins), or any literal CSS color for a one-off custom tone. In the default `all` scope it colors the selected-ring fill + dot and the unselected ring border. Named tones track light/dark mode. Set `toneScope="selected"` to tone only the selected option and leave the rest neutral. The option text stays neutral — recolor it in plain CSS via the `--text-N-{tone}` tokens. |
273
- | `toneScope?` | ToneScope | 'all' | Apply `tone` to every state, or only to the selected option so unselected rings stay neutral. An option's own `toneScope` wins. |
275
+ | `toneScope?` | 'all' \| 'selected' | 'all' | Apply `tone` to every state, or only to the selected option so unselected rings stay neutral. An option's own `toneScope` wins. |
274
276
  | `value?` | string | — | Controlled selected value. The group follows this prop and reports a requested change through `onStateChange`. Leave it undefined for uncontrolled use. |
275
277
 
276
278
  ## Web Component