@antadesign/anta 0.3.18 → 0.3.20

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 (92) hide show
  1. package/README.md +38 -242
  2. package/dist/anta_helpers.d.ts +2 -2
  3. package/dist/components/Banner.d.ts +3 -3
  4. package/dist/components/Button.d.ts +1 -1
  5. package/dist/components/Calendar.d.ts +11 -12
  6. package/dist/components/Card.d.ts +1 -1
  7. package/dist/components/Card.js +1 -1
  8. package/dist/components/Checkbox.d.ts +8 -9
  9. package/dist/components/Dialog.d.ts +1 -1
  10. package/dist/components/Expander.d.ts +5 -6
  11. package/dist/components/Input.d.ts +6 -3
  12. package/dist/components/Input.js +3 -1
  13. package/dist/components/InputAutocomplete.d.ts +8 -8
  14. package/dist/components/InputDate.css +1 -0
  15. package/dist/components/InputDate.d.ts +1 -0
  16. package/dist/components/InputDate.js +5 -5
  17. package/dist/components/InputTime.d.ts +3 -3
  18. package/dist/components/InputTime.js +1 -1
  19. package/dist/components/Menu.d.ts +7 -3
  20. package/dist/components/Menu.js +3 -2
  21. package/dist/components/MenuItem.d.ts +1 -1
  22. package/dist/components/MenuItem.js +1 -1
  23. package/dist/components/Progress.d.ts +1 -1
  24. package/dist/components/RadioGroup.d.ts +12 -13
  25. package/dist/components/Select.d.ts +32 -38
  26. package/dist/components/Select.js +9 -13
  27. package/dist/components/SelectFaceted.d.ts +28 -33
  28. package/dist/components/SelectFaceted.js +29 -23
  29. package/dist/components/Slider.d.ts +5 -5
  30. package/dist/components/Switch.d.ts +4 -4
  31. package/dist/components/Tabs.d.ts +7 -7
  32. package/dist/components/Title.d.ts +1 -1
  33. package/dist/components/Toaster.d.ts +1 -1
  34. package/dist/components/Tooltip.d.ts +1 -1
  35. package/dist/components/select-parts.css +1 -0
  36. package/dist/elements/a-card.css +1 -1
  37. package/dist/elements/a-dialog.css +1 -1
  38. package/dist/elements/a-expander.css +1 -1
  39. package/dist/elements/a-expander.d.ts +1 -0
  40. package/dist/elements/a-expander.js +12 -2
  41. package/dist/elements/a-input.css +1 -1
  42. package/dist/elements/a-menu.js +12 -0
  43. package/dist/elements/a-progress.css +1 -1
  44. package/dist/elements/a-slider.css +1 -1
  45. package/dist/elements/a-tabs.css +1 -1
  46. package/dist/elements/a-tabs.d.ts +3 -0
  47. package/dist/elements/a-tabs.js +40 -1
  48. package/dist/elements/a-tooltip.css +1 -1
  49. package/dist/elements/a-tooltip.d.ts +1 -1
  50. package/dist/elements/a-tooltip.js +1 -1
  51. package/dist/elements/index.d.ts +1 -1
  52. package/dist/general_types.d.ts +24 -22
  53. package/dist/index.d.ts +3 -3
  54. package/dist/jsx-runtime.d.ts +9 -0
  55. package/docs/accessibility.md +34 -0
  56. package/docs/changelog.md +561 -0
  57. package/docs/colors.md +42 -0
  58. package/docs/comparison.md +28 -0
  59. package/docs/components/banner.md +267 -0
  60. package/docs/components/button.md +603 -0
  61. package/docs/components/card.md +321 -0
  62. package/docs/components/checkbox.md +296 -0
  63. package/docs/components/dialog.md +314 -0
  64. package/docs/components/expander.md +526 -0
  65. package/docs/components/icon.md +155 -0
  66. package/docs/components/input-autocomplete.md +100 -0
  67. package/docs/components/input-date.md +401 -0
  68. package/docs/components/input-time.md +174 -0
  69. package/docs/components/input.md +610 -0
  70. package/docs/components/loader.md +139 -0
  71. package/docs/components/menu.md +511 -0
  72. package/docs/components/progress.md +182 -0
  73. package/docs/components/radio.md +368 -0
  74. package/docs/components/select-faceted.md +451 -0
  75. package/docs/components/select.md +926 -0
  76. package/docs/components/slider.md +147 -0
  77. package/docs/components/switch.md +198 -0
  78. package/docs/components/tabs.md +471 -0
  79. package/docs/components/tag.md +185 -0
  80. package/docs/components/text.md +148 -0
  81. package/docs/components/title.md +146 -0
  82. package/docs/components/toaster.md +313 -0
  83. package/docs/components/tooltip.md +177 -0
  84. package/docs/credits.md +18 -0
  85. package/docs/index.md +52 -0
  86. package/docs/install-config.md +240 -0
  87. package/docs/normalization.md +102 -0
  88. package/docs/overview.md +10 -0
  89. package/docs/packages/stickers.md +187 -0
  90. package/docs/packages/table.md +153 -0
  91. package/docs/theming.md +13 -0
  92. package/package.json +10 -6
@@ -0,0 +1,526 @@
1
+ # Expander
2
+
3
+ A collapsible disclosure: a header that toggles a region of content open
4
+ and closed. The `Expander` JSX wrapper renders an `<a-expander>`, whose
5
+ shadow DOM is a `<button>` summary plus a collapsible region — so focus,
6
+ keyboard activation (Enter / Space), and the
7
+ WAI-ARIA disclosure semantics (`aria-expanded`) come from the platform
8
+ button. The expand / collapse animation matches the disclosure sections
9
+ used across these docs.
10
+
11
+ The `title` is the always-visible header; the `children` are the
12
+ collapsible body.
13
+
14
+ ## Priority
15
+
16
+ `priority` sets the surface, reusing `Button`/`Text`'s vocabulary.
17
+ `primary` is the most pronounced card; `secondary` (the default) is a
18
+ subtle filled box; `tertiary` is transparent — just the header row and
19
+ its chevron, for an inline disclosure.
20
+
21
+ ```tsx
22
+ <Expander title="Primary" priority="primary">…</Expander>
23
+ <Expander title="Secondary (default)">…</Expander> {/* secondary is the default */}
24
+ <Expander title="Tertiary" priority="tertiary">…</Expander>
25
+ ```
26
+
27
+ ## Round
28
+
29
+ `round` sizes the corners to **half the folded header height** — so a folded
30
+ expander reads as a pill, and an open one keeps that *same* corner radius instead
31
+ of a bigger stadium. The header is measured (a `ResizeObserver`), so the pill
32
+ tracks the header at any `level` or with custom header content. Open the example
33
+ to see the corners hold their radius as the body appears.
34
+
35
+ ```tsx
36
+ <Expander round title="Details" priority="primary">…</Expander>
37
+ ```
38
+
39
+ ## Tones
40
+
41
+ Pick a semantic `tone` to tint the header text and — on the filled
42
+ priorities (`secondary` / `primary`) — the surface too. The set matches
43
+ `Button` / `Tag`: `neutral` (the default) plus `brand`, `info`,
44
+ `success`, `warning`, and `critical`. Color comes from the theme tokens,
45
+ so every tone tracks light and dark mode automatically. Every tone is
46
+ shown below across all three priorities, so you can compare (rows are
47
+ tones, columns are priorities):
48
+
49
+ **Primary**
50
+
51
+ **Secondary** (default)
52
+
53
+ **Tertiary**
54
+
55
+ ```tsx
56
+ // every tone × every priority — e.g. brand:
57
+ <Expander tone="brand" priority="primary" title="Brand" />
58
+ <Expander tone="brand" title="Brand" /> {/* secondary — the default */}
59
+ <Expander tone="brand" priority="tertiary" title="Brand" />
60
+ // …repeat for neutral / info / success / warning / critical
61
+ ```
62
+
63
+ `tone` also accepts **any literal CSS color** for a one-off tint — the
64
+ hue is kept while lightness and chroma are pinned to the named-tone curve
65
+ (and re-tuned for dark mode), the same mechanism as `Button` / `Tag`:
66
+
67
+ ```tsx
68
+ // shown on the `primary` surface — the custom tone tints the fill;
69
+ // drop to "secondary" (the default) for a subtler card, or "tertiary" for text only.
70
+ <Expander tone="#ff1493" priority="primary" title="Magenta" />
71
+ <Expander tone="rebeccapurple" priority="primary" title="Purple" />
72
+ <Expander tone="#0d9488" priority="primary" title="Teal" />
73
+ ```
74
+
75
+ ## Outdent
76
+
77
+ By default the chevron sits **inside** the header row. On a transparent
78
+ `priority="tertiary"` expander, add `outdent` to hang the chevron in the
79
+ left gutter so the **title and body sit flush** with the surrounding
80
+ content — the same layout as the section headers in these docs. Under the
81
+ hood it zeroes `--expander-gutter`: the title and body lose their inset and
82
+ the chevron, positioned in that gutter, auto-hangs to the left. The
83
+ transparent border stays (it keeps tertiary content aligned with the filled
84
+ priorities), and a small internal correction cancels the border's 1px inset
85
+ so the title and body line up exactly with the surrounding content.
86
+
87
+ ```tsx
88
+ <Expander priority="tertiary" outdent title="…">…</Expander>
89
+ ```
90
+
91
+ `outdent` only takes effect on `tertiary` — on the filled priorities
92
+ (`secondary` / `primary`) the container edge has to bound the chevron, so
93
+ it's a no-op there. Use it where the expander sits in a content column
94
+ with room to its left (a prose measure); in a full-width / edge-to-edge
95
+ container the chevron would overhang the edge.
96
+
97
+ ## Title and level
98
+
99
+ `title` takes a string or any node. For a string, `level` (1–6) applies
100
+ the matching heading **type scale** — the same scale as `<Title>` —
101
+ without changing the document outline. Only the type scale changes: the
102
+ title's left alignment is constant across every level (the chevron + a
103
+ fixed 4px edge inset), so swapping `level` never shifts the header
104
+ horizontally.
105
+
106
+ ```tsx
107
+ <Expander title="Level 1" level={1}>…</Expander>
108
+ <Expander title="Level 2" level={2}>…</Expander>
109
+ <Expander title="Level 3" level={3}>…</Expander>
110
+ <Expander title="Level 4" level={4}>…</Expander>
111
+ <Expander title="Level 5 (default)">…</Expander> {/* level 5 is the default */}
112
+ <Expander title="Level 6" level={6}><Text size="small">…</Text></Expander> {/* pair the smallest level with small body text */}
113
+ ```
114
+
115
+ When you need real heading **semantics** — so the title lands in a
116
+ page's table of contents (and assistive tech announces it as a heading) —
117
+ pass a `<Title>` (or an application `<h2>`–`<h6>`) as the `title` instead of a
118
+ string. The heading's `level` sets its type scale, so set it on the heading,
119
+ not on `<Expander>`. A slotted heading keeps its block margins, so reset them
120
+ in the header:
121
+
122
+ ```tsx
123
+ {/* A Title supplies its type scale and heading semantics. Set the level on
124
+ the Title, not on Expander. (The preview uses element composition
125
+ because these docs render statically; the title-prop form is identical.) */}
126
+ <Expander title={<Title level={2}>Title level 2 example</Title>}>…</Expander>
127
+ <Expander title={<Title level={4}>Title level 4 example</Title>}>…</Expander>
128
+ <Expander title={<Title level={6}>Title level 6 example</Title>}>…</Expander>
129
+
130
+ {/* a slotted heading keeps its block-rhythm margins — zero them in the header */}
131
+ <style>{`
132
+ a-expander a-title { margin: 0; }
133
+ `}</style>
134
+ ```
135
+
136
+ ## Controls in the title
137
+
138
+ A control placed **inside** the `title` — a copy button next to the
139
+ heading text, an inline link, an inline `Input` — copies or activates
140
+ without toggling the section. The header refuses to toggle when the click
141
+ landed on any Anta control (`Button` / `ButtonCopy`, `Checkbox`, `Radio`,
142
+ `Input`, `Select`, `InputDate`, `Calendar`, a `Menu` item, a `Tab`) or a
143
+ native control (`button` / `a[href]` / `input` / `select` / `textarea` /
144
+ `label` / editable), so a `ButtonCopy` beside the title works with no
145
+ extra wiring:
146
+
147
+ ```tsx
148
+ {/* A ButtonCopy in the title copies without toggling — no stopPropagation needed. */}
149
+ <Expander title={<>Debug timeline <ButtonCopy copy={moment} icon="copy" size="small" priority="tertiary" /></>}>
150
+ …
151
+ </Expander>
152
+ ```
153
+
154
+ The decision is read from the click's composed path synchronously in the
155
+ element, so it holds even where event handlers run off the UI thread (a
156
+ consumer's `stopPropagation()` can arrive too late to beat the toggle) —
157
+ the same contract as `Menu`'s close-on-outside-click.
158
+
159
+ A **disabled** control is inert, so its click toggles the section like the
160
+ title text does — the exclusion applies only to live controls. For a
161
+ control the detection can't recognize (a non-interactive custom widget
162
+ with its own handler), mark it `data-expander-ignore` to opt it out of the
163
+ toggle:
164
+
165
+ ```tsx
166
+ <Expander title={<>Report <MyWidget data-expander-ignore /></>}>…</Expander>
167
+ ```
168
+
169
+ ## Actions
170
+
171
+ Pass `actions` to render extra controls — buttons, tags — at the end of
172
+ the header row. They sit **outside** the toggle trigger, as its flex
173
+ siblings: clicking them never toggles the expander, they're separately
174
+ focusable, and screen readers announce them as their own controls.
175
+ (Don't put interactive elements *inside* a custom `title` instead —
176
+ nesting controls in a button is an accessibility anti-pattern.)
177
+
178
+ ```tsx
179
+ <Expander title="Build artifacts" actions={<Button size="small" label="Download" />}>
180
+ …
181
+ </Expander>
182
+ ```
183
+
184
+ The header never wraps: actions keep their intrinsic size and the
185
+ title ellipsizes first, so keep actions compact (a small button or
186
+ two, a tag). Pass several controls as a fragment — they form a row
187
+ with a 2px gap:
188
+
189
+ ```tsx
190
+ <Expander
191
+ title="A very long expandable section title that shows how the actions would push content to ellipsize when space runs out"
192
+ actions={<><Button size="small" label="Expand" /><Button size="small" label="Share" /><Button size="small" label="Download" /></>}
193
+ >
194
+ …
195
+ </Expander>
196
+ ```
197
+
198
+ Actions stay visible in the folded state — the header represents the
199
+ section whether it's open or not (the same convention as GitHub's file
200
+ headers). If an action only makes sense while open, drive the expander
201
+ in controlled mode and swap the `actions` node on toggle.
202
+
203
+ ## Disabled
204
+
205
+ `disabled` freezes the expander: the header is no longer clickable or
206
+ focusable, the hover affordance is off, and the text dims. The open
207
+ state stays as-is — disabling an open expander keeps it open. Header
208
+ `actions` stay live; disable them separately if needed.
209
+
210
+ ```tsx
211
+ <Expander title="Disabled, closed" disabled>…</Expander>
212
+ <Expander title="Disabled, open" disabled defaultOpen>…</Expander>
213
+ ```
214
+
215
+ ## Open state
216
+
217
+ For uncontrolled use, pass `defaultOpen` for the initial state. The element
218
+ then updates its open state after interaction. For controlled use, pass `open`
219
+ and handle `onStateChange`. The expander follows the `open` prop and reports a
220
+ requested change. Update application state with `detail.next` to accept it, or
221
+ leave the state unchanged to reject it.
222
+
223
+ `onStateChange` is event-first: `(event, { next, prev })`, where `next`/`prev`
224
+ are booleans (the requested / previous open state). `statechange` fires
225
+ **before** the element applies anything and is **cancelable**, so even
226
+ uncontrolled you can veto a toggle synchronously with `event.preventDefault()`
227
+ (e.g. a section that confirms before closing) — no need to lift state.
228
+
229
+ ```tsx
230
+ // uncontrolled
231
+ <Expander title="Details" defaultOpen>…</Expander>
232
+
233
+ // uncontrolled, with a veto
234
+ <Expander title="Details" onStateChange={(e, { next }) => {
235
+ if (!next && unsaved) e.preventDefault() // refuse to close
236
+ }}>…</Expander>
237
+
238
+ // controlled
239
+ const [open, setOpen] = useState(false)
240
+ <Expander title="Details" open={open} onStateChange={(e, { next }) => setOpen(next)}>…</Expander>
241
+ ```
242
+
243
+ ## Controlled vs Uncontrolled
244
+
245
+ Open state follows Anta's shared **state contract**. In uncontrolled use, the
246
+ element updates open state. In controlled use, the application supplies it. The
247
+ `<Expander>` wrapper uses booleans; the `<a-expander>` element uses a `state`
248
+ string. The wrapper maps between them:
249
+
250
+ | | `<Expander>` (JSX) | `<a-expander>` (element) |
251
+ |---|---|---|
252
+ | **Uncontrolled** | `defaultOpen` | `default-state="open" \| "closed"` |
253
+ | **Controlled** | `open` + `onStateChange` | `state="open" \| "closed"` + `statechange` |
254
+ | **Change event** | `onStateChange(event, { next, prev })` — `next` / `prev` are **booleans** | `statechange`, a `CustomEvent<{ next, prev }>` — `'open'` / `'closed'` |
255
+
256
+ `statechange` is **cancelable** and fires *before* the element applies the
257
+ change. Uncontrolled, call `event.preventDefault()` to veto a toggle (e.g.
258
+ confirm before closing). Controlled, the element never self-applies — apply
259
+ `detail.next` to `open` to accept, or do nothing to reject.
260
+
261
+ ### Accordion
262
+
263
+ An accordion is the controlled pattern applied to a stack of expanders that share one
264
+ open value: hold the open item's id in state, point each `open` at it, and on
265
+ `onStateChange` set that id (or `null`) — so opening one folds the rest. Wrapping them
266
+ in a `.accordion` lets you square the inner corners and overlap the borders by 1px, so
267
+ the stack reads as one panel.
268
+
269
+ ```tsx
270
+ const ITEMS = [
271
+ { id: 'shipping', title: 'Shipping & delivery', body: '…' },
272
+ { id: 'returns', title: 'Returns', body: '…' },
273
+ { id: 'warranty', title: 'Warranty', body: '…' },
274
+ ]
275
+
276
+ function Accordion() {
277
+ const [openId, setOpenId] = useState('shipping') // null = all closed
278
+ return (
279
+ <div className="accordion">
280
+ {ITEMS.map((item) => (
281
+ <Expander
282
+ key={item.id}
283
+ title={item.title}
284
+ open={openId === item.id}
285
+ onStateChange={(_e, { next }) => setOpenId(next ? item.id : null)}
286
+ >
287
+ <Text>{item.body}</Text>
288
+ </Expander>
289
+ ))}
290
+ </div>
291
+ )
292
+ }
293
+ ```
294
+
295
+ ```css
296
+ .accordion { display: flex; flex-direction: column; }
297
+
298
+ /* Overlap adjacent borders so they share one 1px line. */
299
+ .accordion a-expander:not(:first-child) { margin-top: -1px; }
300
+
301
+ /* Square the inner corners so the stack reads as one panel. */
302
+ .accordion a-expander:first-child { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
303
+ .accordion a-expander:last-child { border-top-left-radius: 0; border-top-right-radius: 0; }
304
+ .accordion a-expander:not(:first-child):not(:last-child) { border-radius: 0; }
305
+ ```
306
+
307
+ ### Props
308
+
309
+ | Prop | Type | Default | Description |
310
+ |------|------|---------|-------------|
311
+ | `title` | ReactNode | — | Summary (header) content. A string is rendered with the `level`
312
+ type scale; pass a node (e.g. a `<Title>`) for full control or real
313
+ heading semantics in the document outline. |
314
+ | `actions?` | ReactNode | — | Header actions (e.g. buttons, tags) rendered at the end of the
315
+ header row, OUTSIDE the toggle trigger — clicking them never
316
+ toggles, they're separately focusable, and screen readers see them
317
+ as separate controls. |
318
+ | `defaultOpen?` | boolean | — | Initial open state for the uncontrolled case. |
319
+ | `disabled?` | boolean | — | Disables the header: not clickable or focusable, hover affordance
320
+ off, text dimmed. The open state freezes as-is — disabling an open
321
+ expander keeps it open. `actions` stay live; disable them
322
+ separately if needed. |
323
+ | `level?` | 1 \| 2 \| 3 \| 4 \| 5 \| 6 | 5 | Heading type scale applied to a string `title` (mirrors `<Title>`'s
324
+ levels). Visual only — for outline semantics, pass a `<Title>` as
325
+ the title. |
326
+ | `onStateChange?` | (event, detail) => void | — | Fired before the open state changes. `event` is the cancelable
327
+ `statechange` — call `event.preventDefault()` to veto an *uncontrolled*
328
+ toggle (e.g. confirm before closing). `detail.next` is the requested
329
+ open state, `detail.prev` the current one (booleans). In controlled
330
+ mode, apply `detail.next` to `open` to accept, or do nothing to reject. |
331
+ | `open?` | boolean | — | Controlled open state. When provided, the consumer owns open/close. Selecting
332
+ the summary requests a change through `onStateChange`; omit `open` for
333
+ uncontrolled use. |
334
+ | `outdent?` | boolean | — | Outdent the chevron into the left gutter so the title and body sit
335
+ flush with surrounding content (the docs-header layout). Only takes
336
+ effect with `priority="tertiary"` — on the filled priorities the
337
+ container edge has to bound the chevron, so it's a no-op there. |
338
+ | `priority?` | 'primary' \| 'secondary' \| 'tertiary' | 'secondary' | Surface emphasis. `secondary` (the default) is a subtle fill;
339
+ `primary` is a more pronounced card; `tertiary` is transparent (the
340
+ bare disclosure). |
341
+ | `round?` | boolean \| number \| string | — | Round corners sized to half the folded (header) height: a pill when folded,
342
+ and — when expanded — the same corner radius rather than a bigger stadium.
343
+ The element measures the header, so it tracks custom header content. Pass a
344
+ `number` (px) or CSS length string for a *fixed* radius that overrides the
345
+ measurement (and skips it) in both states. |
346
+ | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Semantic tone, or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`)
347
+ for a one-off custom tone. Named tones re-point the text and (on filled
348
+ priorities) the surface to the matching palette; a custom color keeps
349
+ its hue while lightness/chroma are pinned. `'neutral'` (the default) is
350
+ the same as omitting it. |
351
+
352
+ Use the web component directly when you are not using React or Preact and a native control does not fit.
353
+
354
+ Slots hold the title and actions. The details element contains the collapsible body.
355
+
356
+ ```html
357
+ <a-expander default-state="open">
358
+ <a-expander-summary slot="title">Build artifacts</a-expander-summary>
359
+ <a-button slot="actions" role="button" tabindex="0" size="small">
360
+ <a-button-label>Share</a-button-label>
361
+ </a-button>
362
+ <a-expander-details>Everything without a slot attribute is the body.</a-expander-details>
363
+ </a-expander>
364
+ ```
365
+
366
+ ### Native HTML details
367
+
368
+ For a simple disclosure, add `data-anta` to native `<details>`. It keeps the
369
+ browser's opening, keyboard, and accessibility behavior while taking on the
370
+ neutral tertiary Expander treatment. Add `priority="secondary"` or `"primary"`
371
+ for a filled surface. `round`, `tone`, tertiary `outdent`, and `level="1"`–`"6"`
372
+ use the matching Expander visual treatments. `level` changes visual type only;
373
+ use a heading inside the summary when you need heading semantics.
374
+
375
+ ```html
376
+ <details data-anta priority="secondary" tone="info" round level="3" open>
377
+ <summary>Build artifacts</summary>
378
+ <div>Everything inside &lt;details&gt; is the collapsible body.</div>
379
+ </details>
380
+ ```
381
+
382
+ Reach for the props first: **`tone`** sets the color (any CSS color for a custom
383
+ tone — it derives the surface in oklch), **`priority`** the fill, **`level`** the
384
+ heading. Two knobs are worth keeping: **`--expander-tone-source`** (the color a
385
+ custom `tone` derives from — set it to drive the palette from your own variable) and
386
+ **`--expander-gutter`** (the shared left inset that re-insets header + body + chevron
387
+ in lockstep; `outdent` sets it to `0`).
388
+
389
+ ```tsx
390
+ <Expander tone="#e0457b" title="Custom" />
391
+ ```
392
+
393
+ For everything else, `<a-expander>` exposes shadow **parts** — `::part(summary)` (the
394
+ header button), `::part(content)` (the body), `::part(actions)` (the actions row) — and
395
+ the light-DOM `<a-expander-summary>` / `<a-expander-details>` children, which you style
396
+ directly. The header hover / active / focus states and the chevron all live on
397
+ `::part(summary)`.
398
+
399
+ Each example keeps its CSS in a `<style>` **inside** its own preview, targeting a demo
400
+ class on that preview's expander (e.g. `.hide-chevron`) — so the rule you read is
401
+ literally in the preview's DOM, next to the element it styles, never a page-wide
402
+ stylesheet. The folded recipe under each preview is that same CSS; in your app, swap the
403
+ demo class for your own selector.
404
+
405
+ **Hide the chevron.** There's deliberately no prop for it: a region that folds needs a
406
+ visible affordance. For a genuine one-off, the chevron is the summary's `::before`, so
407
+ remove it — then pull `--expander-gutter` in (it drives the title *and* body inset
408
+ together) to tighten the now-empty gutter, and drop your own marker into the title:
409
+
410
+ ```css
411
+ a-expander.hide-chevron::part(summary)::before { display: none; } /* remove the built-in chevron */
412
+ a-expander.hide-chevron { --expander-gutter: 12px; } /* tighten the inset (default 24px) */
413
+ ```
414
+
415
+ **Swap the chevron glyph.** It's a CSS mask, so point `mask-image` at your own SVG; it
416
+ keeps its gutter position and the open-state rotation:
417
+
418
+ ```css
419
+ /* the chevron is the summary's ::before with a mask-image; swap the glyph for a
420
+ solid triangle — it keeps its position and the open-state rotate */
421
+ a-expander.swap-chevron::part(summary)::before {
422
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 5l11 7-11 7z'/%3E%3C/svg%3E");
423
+ }
424
+ ```
425
+
426
+ **Tint the whole header** on hover / press by styling the summary part (the header
427
+ `<button>`):
428
+
429
+ ```css
430
+ a-expander.hover-tint::part(summary):hover { background: var(--bg-3); }
431
+ a-expander.hover-tint::part(summary):active { background: var(--bg-2); }
432
+ ```
433
+
434
+ **Highlight the title on hover.** The title inherits its `color` from `::part(summary)`,
435
+ so recolor the part on hover and the title (and the `currentColor` chevron) follow — a
436
+ custom title node that sets its own `color` opts out. Here we also drop the tertiary
437
+ hover underline on the light-DOM `a-expander-summary`:
438
+
439
+ ```css
440
+ a-expander.title-highlight::part(summary):hover { color: #e5484d; } /* title color on hover */
441
+ a-expander.title-highlight a-expander-summary { text-decoration: none; } /* drop the hover underline */
442
+ ```
443
+
444
+ **React to open / closed state** with the **`:state(open)`** custom state — your CSS can
445
+ respond to it in controlled and uncontrolled mode alike, no JS. Two worked examples.
446
+
447
+ *A custom code block* — pointed at the code-block surface tokens so it reads like the
448
+ foldable code blocks on this site: a darker header bar over a lighter content surface, a
449
+ `border-5` frame, and a tertiary-button copy that fades in once open.
450
+
451
+ ```tsx
452
+ {/* An Expander pointed at the code-block surface tokens so it reads like the
453
+ foldable code blocks on this site: a darker header bar over a lighter
454
+ content surface, a border-5 frame, and a tertiary-button copy that fades
455
+ in once open. */}
456
+ <Expander
457
+ title="Example.tsx"
458
+ actions={<Button priority="tertiary"><Icon shape="copy" /></Button>}
459
+ className="code-like"
460
+ defaultOpen
461
+ >
462
+ <pre>{`import { Button } from '@antadesign/anta'
463
+
464
+ export function SaveButton() {
465
+ return <Button priority="primary">Save</Button>
466
+ }`}</pre>
467
+ </Expander>
468
+
469
+ <style>{`
470
+ a-expander.code-like {
471
+ background: var(--bg-pane); /* header — a bit darker than the content */
472
+ border-color: var(--border-5); /* the code-block frame outline */
473
+ color: var(--text-2); /* title + code */
474
+
475
+ /* With custom corner radius don't reach for overflow:hidden (it would clip
476
+ the focus ring); instead give the host the radius and match it on the
477
+ parts: ::part(summary) the top corners, the body the bottom corners. */
478
+ border-radius: 0;
479
+ }
480
+ a-expander.code-like::part(summary):hover { color: var(--text-1); } /* title brightens on hover */
481
+ a-expander.code-like::part(summary) { min-height: 36px; }
482
+ a-expander.code-like::part(actions) { gap: 6px; } /* roomier action row */
483
+ a-expander.code-like a-expander-summary {
484
+ font-size: 14px; /* the title, a touch bigger */
485
+ letter-spacing: 0.02em;
486
+ }
487
+ a-expander.code-like::part(content) { background: var(--bg-canvas); } /* content — lighter than the header */
488
+ a-expander.code-like a-expander-details {
489
+ padding: 12px 16px 14px; /* roomier top padding above the code */
490
+ font-family: var(--monospace);
491
+ font-size: 13px;
492
+ font-weight: 440;
493
+ line-height: 20px;
494
+ }
495
+ a-expander.code-like a-expander-details pre { margin: 0; font: inherit; }
496
+ a-expander.code-like [slot="actions"] { opacity: 0; transition: opacity 150ms ease; } /* copy appears once open */
497
+ a-expander.code-like:state(open) [slot="actions"] { opacity: 1; }
498
+ `}</style>
499
+ ```
500
+
501
+ *A chevron as a status indicator* — drop the built-in toggle chevron and put a
502
+ **non-interactive** chevron in `actions` instead. The whole header still toggles; the
503
+ indicator only reflects state, rotating via `:state(open)`. With the `<Expander>` JSX
504
+ wrapper, pass `actions={<Icon shape="chevron-down" />}` (the preview below uses the
505
+ equivalent element composition because these docs render statically):
506
+
507
+ ```tsx
508
+ {/* renders an <a-expander> whose only header control is a non-interactive
509
+ chevron in the actions slot; the whole header toggles, the chevron flips */}
510
+ <Expander actions={<Icon shape="chevron-down" />} className="indicator" defaultOpen>
511
+ Build log
512
+ </Expander>
513
+
514
+ {/* the JSX wrapper boxes the icon in a layout-neutral span, so target the
515
+ [slot="actions"] element */}
516
+ <style>{`
517
+ a-expander.indicator { --expander-gutter: 12px; } /* one var: title + body inset */
518
+ a-expander.indicator::part(summary)::before { display: none; } /* drop the built-in chevron */
519
+ a-expander.indicator [slot="actions"] {
520
+ pointer-events: none;
521
+ margin-inline-end: 6px; /* a little more right inset */
522
+ transition: transform 150ms ease;
523
+ }
524
+ a-expander.indicator:state(open) [slot="actions"] { transform: rotate(180deg); } /* points down → up */
525
+ `}</style>
526
+ ```
@@ -0,0 +1,155 @@
1
+ # Icon
2
+
3
+ Inline-block icon rendered via CSS `mask-image`. Color follows
4
+ `currentColor`, default size is 16×16, and the only required prop is
5
+ `shape`. The set of valid shapes is the union of Anta's built-ins plus
6
+ any custom icons you generate yourself.
7
+
8
+ `shape="loader"` is the exception: it is Anta's animated loading alias for
9
+ string-only icon props. Use `Loader` when you need a progress value, speed, or
10
+ accessible loading status.
11
+
12
+ > In practice you'll rarely reach for `<Icon>` directly. Most Anta
13
+ > components that need an icon expose an `icon` prop that's typed
14
+ > against `IconShape` — autocomplete and type-checking work the same
15
+ > way without the wrapping element. Use `<Icon>` when you need a
16
+ > standalone icon outside of any other component.
17
+
18
+ ## Built-in shapes
19
+
20
+ ### Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |------|------|---------|-------------|
24
+ | `shape` | IconShape | — | Which icon to render. The set of valid shapes comes from Anta's
25
+ built-in icons plus any consumer-generated shapes (via the
26
+ `IconShapes` interface module augmentation). |
27
+ | `label?` | string | — | Accessible name for the icon. When set, the wrapper exposes
28
+ `role="img"` and `aria-label={label}` so screen readers announce
29
+ the icon. When omitted (the default), the icon is treated as
30
+ decorative — `aria-hidden="true"` is applied so it doesn't add
31
+ noise alongside neighbouring text. |
32
+ | `size?` | number | 16 | Width and height in pixels. |
33
+
34
+ Use the web component directly when you are not using React or Preact and a native control does not fit.
35
+
36
+ Set the accessible name on the raw element. Use `--icon-size` for a portable custom
37
+ size.
38
+
39
+ ```html
40
+ <a-icon
41
+ shape="warning-triangle"
42
+ role="img"
43
+ aria-label="Warning"
44
+ style="--icon-size: 24px"
45
+ ></a-icon>
46
+ ```
47
+
48
+ ## Sizing and color
49
+
50
+ Pass `size` (a number, in pixels) to control width and height
51
+ together. Default is `16`. Color always follows `currentColor`.
52
+
53
+ ```tsx
54
+ <Icon shape="chevron-down" /> {/* 16×16, current color */}
55
+ <Icon shape="check" size={24} /> {/* 24×24 */}
56
+ <Text tone="critical"><Icon shape="warning-triangle" /></Text> {/* tinted */}
57
+ ```
58
+
59
+ Internally, `size` is applied as the `--icon-size` CSS custom property
60
+ on the rendered `<a-icon>`. The base CSS rule reads it as
61
+ `width: var(--icon-size, 16px); height: var(--icon-size, 16px)`. That
62
+ means consumer CSS (or a parent's variable cascade) can drive icon
63
+ size too — useful for sizing whole regions of UI at once:
64
+
65
+ ```css
66
+ .toolbar { --icon-size: 18px; }
67
+ ```
68
+
69
+ > The `<Icon size>` wrapper sets `--icon-size` inline, so it works
70
+ > everywhere. Hand-authoring the raw element's `size` *attribute*
71
+ > (`<a-icon size="18">`) instead relies on typed `attr()` (Chrome 133+);
72
+ > on browsers without it (iOS Safari, Firefox) the attribute is ignored
73
+ > and the icon falls back to the 16px default. Prefer `<Icon size>` for
74
+ > exact custom sizes.
75
+
76
+ ## The `blank` spacer
77
+
78
+ `shape="blank"` renders nothing but still occupies a full icon box at the
79
+ current `size`. Use it to reserve a leading-icon column so labels line up when
80
+ only some rows carry an icon — a menu or list where a few items have a leading
81
+ glyph and the rest shouldn't shift.
82
+
83
+ ```tsx
84
+ <Icon shape="blank" /> {/* invisible 16×16 spacer */}
85
+ <MenuItem icon="blank" label="No icon, but aligned" />
86
+ ```
87
+
88
+ Note this differs from omitting the icon: leaving `shape` (or `MenuItem`'s
89
+ `icon`) unset renders **no element**, while `blank` renders an invisible sized
90
+ box. A bare `<Icon>` with no shape is *not* a spacer — with no mask it paints a
91
+ solid square.
92
+
93
+ ## Adding your own icons
94
+
95
+ Anta ships a generator script at `dist/generate-icons.mjs`. Drop your
96
+ SVGs in a folder, point the script at it, and it emits a CSS file plus
97
+ a TypeScript declaration that augments Anta's `IconShapes` interface —
98
+ your shapes become valid `<Icon shape="…" />` values automatically,
99
+ with autocomplete.
100
+
101
+ ```sh
102
+ node ./node_modules/@antadesign/anta/dist/generate-icons.mjs \
103
+ --input ./svgs \
104
+ --output ./src/icons \
105
+ --name my-icons
106
+ ```
107
+
108
+ `--input` and `--output` are arbitrary paths in your project — pick
109
+ whatever fits your layout. The example above writes
110
+ `./src/icons/my-icons.css` and `./src/icons/my-icons.d.ts`. Import
111
+ the CSS once at runtime; the `.d.ts` only needs to be in your
112
+ TypeScript include path (the same `tsconfig.json` `include` that
113
+ already covers your other source files), nothing else.
114
+
115
+ ```ts
116
+ import './icons/my-icons.css' // runtime: registers shape rules on <a-icon>
117
+ ```
118
+
119
+ You can now use `<Icon shape="my-shape" />` with full type safety.
120
+
121
+ ### SVG conventions
122
+
123
+ The generator strips `width=` and `height=` from the root `<svg>` so
124
+ size is fully under the host element's CSS. For best results:
125
+
126
+ - Use `viewBox="0 0 16 16"` (or any square viewBox).
127
+ - Use `stroke="currentColor"` (or `fill="currentColor"`) — the icon is
128
+ recolored via CSS `background-color: currentColor` masked through the
129
+ SVG's alpha. Black/colored fills work too, but only the alpha
130
+ matters; the shape is always rendered in the current text color.
131
+ - Single-color icons only. Multi-color SVGs collapse to one color
132
+ because the entire shape is masked through `currentColor`.
133
+
134
+ ### Conflicting shape names
135
+
136
+ If a generated shape has the same name as one of Anta's built-ins
137
+ (e.g. `chevron-down`), the generator prints a warning. At runtime, the
138
+ CSS file imported **last** wins — so importing your generated CSS
139
+ after `@antadesign/anta/elements` overrides the matching built-ins.
140
+ TypeScript silently merges the duplicate keys; both names remain valid
141
+ at the type level.
142
+
143
+ ### Programmatic use
144
+
145
+ ```ts
146
+ import { generate } from '@antadesign/anta/generate-icons.mjs'
147
+
148
+ await generate({
149
+ input: './svgs',
150
+ output: './src/icons',
151
+ name: 'my-icons',
152
+ })
153
+ ```
154
+
155
+ Useful for wiring icon generation into a build script.