@antadesign/anta 0.3.17 → 0.3.19

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 (77) hide show
  1. package/README.md +38 -242
  2. package/dist/components/Input.d.ts +4 -1
  3. package/dist/components/Input.js +2 -0
  4. package/dist/components/InputDate.css +1 -0
  5. package/dist/components/InputDate.d.ts +1 -0
  6. package/dist/components/InputDate.js +5 -5
  7. package/dist/components/Loader.d.ts +39 -0
  8. package/dist/components/Loader.js +33 -0
  9. package/dist/components/Menu.d.ts +5 -1
  10. package/dist/components/Menu.js +3 -2
  11. package/dist/components/Progress.d.ts +17 -8
  12. package/dist/components/Progress.js +12 -9
  13. package/dist/components/Select.d.ts +1 -0
  14. package/dist/components/Select.js +9 -13
  15. package/dist/components/SelectFaceted.d.ts +1 -0
  16. package/dist/components/SelectFaceted.js +29 -23
  17. package/dist/components/select-parts.css +1 -0
  18. package/dist/elements/a-button.css +1 -1
  19. package/dist/elements/a-icon.d.ts +1 -0
  20. package/dist/elements/a-icon.js +1 -0
  21. package/dist/elements/a-input.css +1 -1
  22. package/dist/elements/a-loader.css +1 -0
  23. package/dist/elements/a-loader.d.ts +12 -0
  24. package/dist/elements/a-loader.js +15 -0
  25. package/dist/elements/a-menu.js +12 -0
  26. package/dist/elements/a-progress.css +1 -1
  27. package/dist/elements/a-progress.js +32 -2
  28. package/dist/elements/a-slider.css +1 -1
  29. package/dist/elements/a-tabs.css +1 -1
  30. package/dist/elements/a-tabs.d.ts +3 -0
  31. package/dist/elements/a-tabs.js +40 -1
  32. package/dist/elements/index.d.ts +1 -0
  33. package/dist/elements/index.js +3 -0
  34. package/dist/general_types.d.ts +27 -2
  35. package/dist/index.d.ts +6 -1
  36. package/dist/index.js +2 -0
  37. package/dist/jsx-runtime.d.ts +11 -1
  38. package/dist/reset.css +1 -1
  39. package/dist/theme-anta.css +1 -1
  40. package/docs/accessibility.md +34 -0
  41. package/docs/changelog.md +551 -0
  42. package/docs/colors.md +42 -0
  43. package/docs/comparison.md +28 -0
  44. package/docs/components/banner.md +267 -0
  45. package/docs/components/button.md +603 -0
  46. package/docs/components/card.md +322 -0
  47. package/docs/components/checkbox.md +297 -0
  48. package/docs/components/dialog.md +314 -0
  49. package/docs/components/expander.md +529 -0
  50. package/docs/components/icon.md +155 -0
  51. package/docs/components/input-autocomplete.md +101 -0
  52. package/docs/components/input-date.md +406 -0
  53. package/docs/components/input-time.md +174 -0
  54. package/docs/components/input.md +610 -0
  55. package/docs/components/loader.md +139 -0
  56. package/docs/components/menu.md +447 -0
  57. package/docs/components/progress.md +182 -0
  58. package/docs/components/radio.md +369 -0
  59. package/docs/components/select-faceted.md +456 -0
  60. package/docs/components/select.md +931 -0
  61. package/docs/components/slider.md +142 -0
  62. package/docs/components/switch.md +198 -0
  63. package/docs/components/tabs.md +471 -0
  64. package/docs/components/tag.md +185 -0
  65. package/docs/components/text.md +148 -0
  66. package/docs/components/title.md +146 -0
  67. package/docs/components/toaster.md +313 -0
  68. package/docs/components/tooltip.md +177 -0
  69. package/docs/credits.md +22 -0
  70. package/docs/index.md +52 -0
  71. package/docs/install-config.md +240 -0
  72. package/docs/normalization.md +105 -0
  73. package/docs/overview.md +10 -0
  74. package/docs/packages/stickers.md +187 -0
  75. package/docs/packages/table.md +153 -0
  76. package/docs/theming.md +13 -0
  77. package/package.json +10 -6
@@ -0,0 +1,529 @@
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 & 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 your own `<h2>`–`<h6>`) as the `title` instead of a
118
+ string. The node owns its own type scale, so set the level on it, not on
119
+ `<Expander>`; a slotted heading keeps its block-rhythm margins, so zero
120
+ them in the header:
121
+
122
+ ```tsx
123
+ {/* A node title owns its own scale + 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
+ Leave it uncontrolled and pass `defaultOpen` for the initial state — the
218
+ element owns open/close from there. To drive it yourself, pass `open`
219
+ and handle `onStateChange` (controlled): the expander then only follows
220
+ the prop, and clicking the summary just *requests* a change — apply
221
+ `detail.next` to your state to accept, or ignore it to reject. If you pass
222
+ `open`, you own it.
223
+
224
+ `onStateChange` is event-first: `(event, { next, prev })`, where `next`/`prev`
225
+ are booleans (the requested / previous open state). `statechange` fires
226
+ **before** the element applies anything and is **cancelable**, so even
227
+ uncontrolled you can veto a toggle synchronously with `event.preventDefault()`
228
+ (e.g. a section that confirms before closing) — no need to lift state.
229
+
230
+ ```tsx
231
+ // uncontrolled
232
+ <Expander title="Details" defaultOpen>…</Expander>
233
+
234
+ // uncontrolled, with a veto
235
+ <Expander title="Details" onStateChange={(e, { next }) => {
236
+ if (!next && unsaved) e.preventDefault() // refuse to close
237
+ }}>…</Expander>
238
+
239
+ // controlled
240
+ const [open, setOpen] = useState(false)
241
+ <Expander title="Details" open={open} onStateChange={(e, { next }) => setOpen(next)}>…</Expander>
242
+ ```
243
+
244
+ ## Controlled vs Uncontrolled
245
+
246
+ Open state follows Anta's shared **state contract**. Leave it *uncontrolled* and
247
+ the expander owns open/close; pass the controlled prop and *you* own it. The two
248
+ layers speak different idioms — the `<Expander>` wrapper takes booleans, the
249
+ `<a-expander>` element takes a `state` string enum — and the wrapper maps between
250
+ them:
251
+
252
+ | | `<Expander>` (JSX) | `<a-expander>` (element) |
253
+ |---|---|---|
254
+ | **Uncontrolled** — element owns it | `defaultOpen` | `default-state="open" \| "closed"` |
255
+ | **Controlled** — you own it | `open` + `onStateChange` | `state="open" \| "closed"` + `statechange` |
256
+ | **Change event** | `onStateChange(event, { next, prev })` — `next` / `prev` are **booleans** | `statechange`, a `CustomEvent<{ next, prev }>` — `'open'` / `'closed'` |
257
+
258
+ `statechange` is **cancelable** and fires *before* the element applies the
259
+ change. Uncontrolled, call `event.preventDefault()` to veto a toggle (e.g.
260
+ confirm before closing). Controlled, the element never self-applies — apply
261
+ `detail.next` to `open` to accept, or do nothing to reject.
262
+
263
+ ### Accordion
264
+
265
+ An accordion is the controlled pattern applied to a stack of expanders that share one
266
+ open value: hold the open item's id in state, point each `open` at it, and on
267
+ `onStateChange` set that id (or `null`) — so opening one folds the rest. Wrapping them
268
+ in a `.accordion` lets you square the inner corners and overlap the borders by 1px, so
269
+ the stack reads as one panel.
270
+
271
+ ```tsx
272
+ const ITEMS = [
273
+ { id: 'shipping', title: 'Shipping & delivery', body: '…' },
274
+ { id: 'returns', title: 'Returns', body: '…' },
275
+ { id: 'warranty', title: 'Warranty', body: '…' },
276
+ ]
277
+
278
+ function Accordion() {
279
+ const [openId, setOpenId] = useState('shipping') // null = all closed
280
+ return (
281
+ <div className="accordion">
282
+ {ITEMS.map((item) => (
283
+ <Expander
284
+ key={item.id}
285
+ title={item.title}
286
+ open={openId === item.id}
287
+ onStateChange={(_e, { next }) => setOpenId(next ? item.id : null)}
288
+ >
289
+ <Text>{item.body}</Text>
290
+ </Expander>
291
+ ))}
292
+ </div>
293
+ )
294
+ }
295
+ ```
296
+
297
+ ```css
298
+ .accordion { display: flex; flex-direction: column; }
299
+
300
+ /* Overlap adjacent borders so they share one 1px line. */
301
+ .accordion a-expander:not(:first-child) { margin-top: -1px; }
302
+
303
+ /* Square the inner corners so the stack reads as one panel. */
304
+ .accordion a-expander:first-child { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
305
+ .accordion a-expander:last-child { border-top-left-radius: 0; border-top-right-radius: 0; }
306
+ .accordion a-expander:not(:first-child):not(:last-child) { border-radius: 0; }
307
+ ```
308
+
309
+ ### Props
310
+
311
+ | Prop | Type | Default | Description |
312
+ |------|------|---------|-------------|
313
+ | `title` | ReactNode | — | Summary (header) content. A string is rendered with the `level`
314
+ type scale; pass a node (e.g. a `<Title>`) for full control or real
315
+ heading semantics in the document outline. |
316
+ | `actions?` | ReactNode | — | Header actions (e.g. buttons, tags) rendered at the end of the
317
+ header row, OUTSIDE the toggle trigger — clicking them never
318
+ toggles, they're separately focusable, and screen readers see them
319
+ as separate controls. |
320
+ | `defaultOpen?` | boolean | — | Initial open state for the uncontrolled case. |
321
+ | `disabled?` | boolean | — | Disables the header: not clickable or focusable, hover affordance
322
+ off, text dimmed. The open state freezes as-is — disabling an open
323
+ expander keeps it open. `actions` stay live; disable them
324
+ separately if needed. |
325
+ | `level?` | 1 \| 2 \| 3 \| 4 \| 5 \| 6 | 5 | Heading type scale applied to a string `title` (mirrors `<Title>`'s
326
+ levels). Visual only — for outline semantics, pass a `<Title>` as
327
+ the title. |
328
+ | `onStateChange?` | (event, detail) => void | — | Fired before the open state changes. `event` is the cancelable
329
+ `statechange` — call `event.preventDefault()` to veto an *uncontrolled*
330
+ toggle (e.g. confirm before closing). `detail.next` is the requested
331
+ open state, `detail.prev` the current one (booleans). In controlled
332
+ mode, apply `detail.next` to `open` to accept, or do nothing to reject. |
333
+ | `open?` | boolean | — | Controlled open state. When provided, the consumer owns open/close:
334
+ the expander only follows this prop, and clicking the summary just
335
+ requests a change via `onStateChange` (so a toggle can be rejected by
336
+ not updating). Leave undefined for uncontrolled. |
337
+ | `outdent?` | boolean | — | Outdent the chevron into the left gutter so the title and body sit
338
+ flush with surrounding content (the docs-header layout). Only takes
339
+ effect with `priority="tertiary"` — on the filled priorities the
340
+ container edge has to bound the chevron, so it's a no-op there. |
341
+ | `priority?` | 'primary' \| 'secondary' \| 'tertiary' | 'secondary' | Surface emphasis. `secondary` (the default) is a subtle fill;
342
+ `primary` is a more pronounced card; `tertiary` is transparent (the
343
+ bare disclosure). |
344
+ | `round?` | boolean \| number \| string | — | Round corners sized to half the folded (header) height: a pill when folded,
345
+ and — when expanded — the same corner radius rather than a bigger stadium.
346
+ The element measures the header, so it tracks custom header content. Pass a
347
+ `number` (px) or CSS length string for a *fixed* radius that overrides the
348
+ measurement (and skips it) in both states. |
349
+ | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Semantic tone, or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`)
350
+ for a one-off custom tone. Named tones re-point the text and (on filled
351
+ priorities) the surface to the matching palette; a custom color keeps
352
+ its hue while lightness/chroma are pinned. `'neutral'` (the default) is
353
+ the same as omitting it. |
354
+
355
+ Use this when you are not using the React or Preact wrapper and a native HTML control does not fit: construct the equivalent Anta web component from the elements below.
356
+
357
+ Slots hold the title and actions. The details element contains the collapsible body.
358
+
359
+ ```html
360
+ <a-expander default-state="open">
361
+ <a-expander-summary slot="title">Build artifacts</a-expander-summary>
362
+ <a-button slot="actions" role="button" tabindex="0" size="small">
363
+ <a-button-label>Share</a-button-label>
364
+ </a-button>
365
+ <a-expander-details>Everything without a slot attribute is the body.</a-expander-details>
366
+ </a-expander>
367
+ ```
368
+
369
+ ### Native HTML details
370
+
371
+ For a simple disclosure, add `data-anta` to native `<details>`. It keeps the
372
+ browser's opening, keyboard, and accessibility behaviour while taking on the
373
+ neutral tertiary Expander treatment. Add `priority="secondary"` or `"primary"`
374
+ for a filled surface. `round`, `tone`, tertiary `outdent`, and `level="1"`–`"6"`
375
+ use the matching Expander visual treatments. `level` changes visual type only;
376
+ use a heading inside the summary when you need heading semantics.
377
+
378
+ ```html
379
+ <details data-anta priority="secondary" tone="info" round level="3" open>
380
+ <summary>Build artifacts</summary>
381
+ <div>Everything inside &lt;details&gt; is the collapsible body.</div>
382
+ </details>
383
+ ```
384
+
385
+ Reach for the props first: **`tone`** sets the colour (any CSS colour for a custom
386
+ tone — it derives the surface in oklch), **`priority`** the fill, **`level`** the
387
+ heading. Two knobs are worth keeping: **`--expander-tone-source`** (the colour a
388
+ custom `tone` derives from — set it to drive the palette from your own variable) and
389
+ **`--expander-gutter`** (the shared left inset that re-insets header + body + chevron
390
+ in lockstep; `outdent` sets it to `0`).
391
+
392
+ ```tsx
393
+ <Expander tone="#e0457b" title="Custom" />
394
+ ```
395
+
396
+ For everything else, `<a-expander>` exposes shadow **parts** — `::part(summary)` (the
397
+ header button), `::part(content)` (the body), `::part(actions)` (the actions row) — and
398
+ the light-DOM `<a-expander-summary>` / `<a-expander-details>` children, which you style
399
+ directly. The header hover / active / focus states and the chevron all live on
400
+ `::part(summary)`.
401
+
402
+ Each example keeps its CSS in a `<style>` **inside** its own preview, targeting a demo
403
+ class on that preview's expander (e.g. `.hide-chevron`) — so the rule you read is
404
+ literally in the preview's DOM, next to the element it styles, never a page-wide
405
+ stylesheet. The folded recipe under each preview is that same CSS; in your app, swap the
406
+ demo class for your own selector.
407
+
408
+ **Hide the chevron.** There's deliberately no prop for it: a region that folds needs a
409
+ visible affordance. For a genuine one-off, the chevron is the summary's `::before`, so
410
+ remove it — then pull `--expander-gutter` in (it drives the title *and* body inset
411
+ together) to tighten the now-empty gutter, and drop your own marker into the title:
412
+
413
+ ```css
414
+ a-expander.hide-chevron::part(summary)::before { display: none; } /* remove the built-in chevron */
415
+ a-expander.hide-chevron { --expander-gutter: 12px; } /* tighten the inset (default 24px) */
416
+ ```
417
+
418
+ **Swap the chevron glyph.** It's a CSS mask, so point `mask-image` at your own SVG; it
419
+ keeps its gutter position and the open-state rotation:
420
+
421
+ ```css
422
+ /* the chevron is the summary's ::before with a mask-image; swap the glyph for a
423
+ solid triangle — it keeps its position and the open-state rotate */
424
+ a-expander.swap-chevron::part(summary)::before {
425
+ 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");
426
+ }
427
+ ```
428
+
429
+ **Tint the whole header** on hover / press by styling the summary part (the header
430
+ `<button>`):
431
+
432
+ ```css
433
+ a-expander.hover-tint::part(summary):hover { background: var(--bg-3); }
434
+ a-expander.hover-tint::part(summary):active { background: var(--bg-2); }
435
+ ```
436
+
437
+ **Highlight the title on hover.** The title inherits its `color` from `::part(summary)`,
438
+ so recolour the part on hover and the title (and the `currentColor` chevron) follow — a
439
+ custom title node that sets its own `color` opts out. Here we also drop the tertiary
440
+ hover underline on the light-DOM `a-expander-summary`:
441
+
442
+ ```css
443
+ a-expander.title-highlight::part(summary):hover { color: #e5484d; } /* title colour on hover */
444
+ a-expander.title-highlight a-expander-summary { text-decoration: none; } /* drop the hover underline */
445
+ ```
446
+
447
+ **React to open / closed state** with the **`:state(open)`** custom state — your CSS can
448
+ respond to it in controlled and uncontrolled mode alike, no JS. Two worked examples.
449
+
450
+ *A custom code block* — pointed at the code-block surface tokens so it reads like the
451
+ foldable code blocks on this site: a darker header bar over a lighter content surface, a
452
+ `border-5` frame, and a tertiary-button copy that fades in once open.
453
+
454
+ ```tsx
455
+ {/* An Expander pointed at the code-block surface tokens so it reads like the
456
+ foldable code blocks on this site: a darker header bar over a lighter
457
+ content surface, a border-5 frame, and a tertiary-button copy that fades
458
+ in once open. */}
459
+ <Expander
460
+ title="Example.tsx"
461
+ actions={<Button priority="tertiary"><Icon shape="copy" /></Button>}
462
+ className="code-like"
463
+ defaultOpen
464
+ >
465
+ <pre>{`import { Button } from '@antadesign/anta'
466
+
467
+ export function SaveButton() {
468
+ return <Button priority="primary">Save</Button>
469
+ }`}</pre>
470
+ </Expander>
471
+
472
+ <style>{`
473
+ a-expander.code-like {
474
+ background: var(--bg-pane); /* header — a bit darker than the content */
475
+ border-color: var(--border-5); /* the code-block frame outline */
476
+ color: var(--text-2); /* title + code */
477
+
478
+ /* With custom corner radius don't reach for overflow:hidden (it would clip
479
+ the focus ring); instead give the host the radius and match it on the
480
+ parts: ::part(summary) the top corners, the body the bottom corners. */
481
+ border-radius: 0;
482
+ }
483
+ a-expander.code-like::part(summary):hover { color: var(--text-1); } /* title brightens on hover */
484
+ a-expander.code-like::part(summary) { min-height: 36px; }
485
+ a-expander.code-like::part(actions) { gap: 6px; } /* roomier action row */
486
+ a-expander.code-like a-expander-summary {
487
+ font-size: 14px; /* the title, a touch bigger */
488
+ letter-spacing: 0.02em;
489
+ }
490
+ a-expander.code-like::part(content) { background: var(--bg-canvas); } /* content — lighter than the header */
491
+ a-expander.code-like a-expander-details {
492
+ padding: 12px 16px 14px; /* roomier top padding above the code */
493
+ font-family: var(--monospace);
494
+ font-size: 13px;
495
+ font-weight: 440;
496
+ line-height: 20px;
497
+ }
498
+ a-expander.code-like a-expander-details pre { margin: 0; font: inherit; }
499
+ a-expander.code-like [slot="actions"] { opacity: 0; transition: opacity 150ms ease; } /* copy appears once open */
500
+ a-expander.code-like:state(open) [slot="actions"] { opacity: 1; }
501
+ `}</style>
502
+ ```
503
+
504
+ *A chevron as a status indicator* — drop the built-in toggle chevron and put a
505
+ **non-interactive** chevron in `actions` instead. The whole header still toggles; the
506
+ indicator only reflects state, rotating via `:state(open)`. With the `<Expander>` JSX
507
+ wrapper, pass `actions={<Icon shape="chevron-down" />}` (the preview below uses the
508
+ equivalent element composition because these docs render statically):
509
+
510
+ ```tsx
511
+ {/* renders an <a-expander> whose only header control is a non-interactive
512
+ chevron in the actions slot; the whole header toggles, the chevron flips */}
513
+ <Expander actions={<Icon shape="chevron-down" />} className="indicator" defaultOpen>
514
+ Build log
515
+ </Expander>
516
+
517
+ {/* the JSX wrapper boxes the icon in a layout-neutral span, so target the
518
+ [slot="actions"] element */}
519
+ <style>{`
520
+ a-expander.indicator { --expander-gutter: 12px; } /* one var: title + body inset */
521
+ a-expander.indicator::part(summary)::before { display: none; } /* drop the built-in chevron */
522
+ a-expander.indicator [slot="actions"] {
523
+ pointer-events: none;
524
+ margin-inline-end: 6px; /* a little more right inset */
525
+ transition: transform 150ms ease;
526
+ }
527
+ a-expander.indicator:state(open) [slot="actions"] { transform: rotate(180deg); } /* points down → up */
528
+ `}</style>
529
+ ```
@@ -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 this when you are not using the React or Preact wrapper and a native HTML control does not fit: construct the equivalent Anta web component from the elements below.
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.