@antadesign/anta 0.3.18 → 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.
- package/README.md +38 -242
- package/dist/components/Input.d.ts +4 -1
- package/dist/components/Input.js +2 -0
- package/dist/components/InputDate.css +1 -0
- package/dist/components/InputDate.d.ts +1 -0
- package/dist/components/InputDate.js +5 -5
- package/dist/components/Menu.d.ts +5 -1
- package/dist/components/Menu.js +3 -2
- package/dist/components/Select.d.ts +1 -0
- package/dist/components/Select.js +9 -13
- package/dist/components/SelectFaceted.d.ts +1 -0
- package/dist/components/SelectFaceted.js +29 -23
- package/dist/components/select-parts.css +1 -0
- package/dist/elements/a-input.css +1 -1
- package/dist/elements/a-menu.js +12 -0
- package/dist/elements/a-slider.css +1 -1
- package/dist/elements/a-tabs.css +1 -1
- package/dist/elements/a-tabs.d.ts +3 -0
- package/dist/elements/a-tabs.js +40 -1
- package/dist/general_types.d.ts +2 -0
- package/dist/jsx-runtime.d.ts +9 -0
- package/docs/accessibility.md +34 -0
- package/docs/changelog.md +551 -0
- package/docs/colors.md +42 -0
- package/docs/comparison.md +28 -0
- package/docs/components/banner.md +267 -0
- package/docs/components/button.md +603 -0
- package/docs/components/card.md +322 -0
- package/docs/components/checkbox.md +297 -0
- package/docs/components/dialog.md +314 -0
- package/docs/components/expander.md +529 -0
- package/docs/components/icon.md +155 -0
- package/docs/components/input-autocomplete.md +101 -0
- package/docs/components/input-date.md +406 -0
- package/docs/components/input-time.md +174 -0
- package/docs/components/input.md +610 -0
- package/docs/components/loader.md +139 -0
- package/docs/components/menu.md +447 -0
- package/docs/components/progress.md +182 -0
- package/docs/components/radio.md +369 -0
- package/docs/components/select-faceted.md +456 -0
- package/docs/components/select.md +931 -0
- package/docs/components/slider.md +142 -0
- package/docs/components/switch.md +198 -0
- package/docs/components/tabs.md +471 -0
- package/docs/components/tag.md +185 -0
- package/docs/components/text.md +148 -0
- package/docs/components/title.md +146 -0
- package/docs/components/toaster.md +313 -0
- package/docs/components/tooltip.md +177 -0
- package/docs/credits.md +22 -0
- package/docs/index.md +52 -0
- package/docs/install-config.md +240 -0
- package/docs/normalization.md +105 -0
- package/docs/overview.md +10 -0
- package/docs/packages/stickers.md +187 -0
- package/docs/packages/table.md +153 -0
- package/docs/theming.md +13 -0
- 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 <details> 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.
|