@antadesign/anta 0.3.25 → 0.3.26

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 (54) hide show
  1. package/dist/anta_helpers.js +3 -3
  2. package/dist/box-types.d.ts +161 -0
  3. package/dist/box-types.js +0 -0
  4. package/dist/bundle.css +1 -1
  5. package/dist/bundle.js +25 -25
  6. package/dist/components/Box.d.ts +37 -0
  7. package/dist/components/Box.js +63 -0
  8. package/dist/elements/a-avatar.css +1 -1
  9. package/dist/elements/a-banner.css +1 -1
  10. package/dist/elements/a-banner.js +4 -4
  11. package/dist/elements/a-box.css +1 -0
  12. package/dist/elements/a-box.d.ts +41 -0
  13. package/dist/elements/a-box.js +576 -0
  14. package/dist/elements/a-button.css +1 -1
  15. package/dist/elements/a-calendar.js +2 -2
  16. package/dist/elements/a-card.css +1 -1
  17. package/dist/elements/a-checkbox.css +1 -1
  18. package/dist/elements/a-dialog.css +1 -1
  19. package/dist/elements/a-expander.css +1 -1
  20. package/dist/elements/a-expander.js +2 -2
  21. package/dist/elements/a-input-time.css +1 -1
  22. package/dist/elements/a-input-time.js +3 -3
  23. package/dist/elements/a-input.css +1 -1
  24. package/dist/elements/a-input.js +6 -6
  25. package/dist/elements/a-menu-item.css +1 -1
  26. package/dist/elements/a-menu-item.js +3 -3
  27. package/dist/elements/a-menu.css +1 -1
  28. package/dist/elements/a-menu.js +2 -2
  29. package/dist/elements/a-radio-group.js +3 -3
  30. package/dist/elements/a-radio.css +1 -1
  31. package/dist/elements/a-slider.css +1 -1
  32. package/dist/elements/a-switch.css +1 -1
  33. package/dist/elements/a-switch.js +2 -2
  34. package/dist/elements/a-tabpanel.js +2 -2
  35. package/dist/elements/a-tabs.css +1 -1
  36. package/dist/elements/a-tabs.js +6 -6
  37. package/dist/elements/a-tag.css +1 -1
  38. package/dist/elements/a-text.css +1 -1
  39. package/dist/elements/a-title.css +1 -1
  40. package/dist/elements/a-toast.js +2 -2
  41. package/dist/elements/a-tooltip.css +1 -1
  42. package/dist/elements/a-tooltip.d.ts +4 -3
  43. package/dist/elements/a-tooltip.js +9 -5
  44. package/dist/elements/index.d.ts +1 -0
  45. package/dist/elements/index.js +3 -0
  46. package/dist/general_types.d.ts +36 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.js +2 -0
  49. package/dist/jsx-runtime.d.ts +2 -1
  50. package/docs/changelog.md +23 -3
  51. package/docs/components/avatar.md +368 -0
  52. package/docs/components/box.md +282 -0
  53. package/docs/index.md +4 -3
  54. package/package.json +1 -1
@@ -0,0 +1,368 @@
1
+ export const initialsOnly = { headRadiusTop: { mode: 'off' }, headRadiusBottom: { mode: 'off' }, bodyBorderRadius: { mode: 'off' } }
2
+
3
+ export const brandRange = { bgColor: { mode: 'range', l: [0.5, 0.62], c: [0.09, 0.15], h: [255, 285] }, harmony: true }
4
+
5
+ export const palette = { bgColor: { mode: 'list', values: ['#6c5ce7', '#00b894', '#e17055', '#0984e3', '#fdcb6e'] }, headRadiusTop: { mode: 'off' }, headRadiusBottom: { mode: 'off' }, bodyBorderRadius: { mode: 'off' } }
6
+
7
+ # Avatar
8
+
9
+ A userpic container. `Avatar` shows one of three things, in priority order: a
10
+ supplied image, a seed-generated placeholder figure, or initials. The generated
11
+ figure imitates a framed portrait — a head and shoulders over a colored
12
+ background — with no face or gender indicators, so an app can fill an empty slot
13
+ with a distinct, on-brand picture per user without an uploaded photo.
14
+
15
+ An alphanumeric `seed` drives the result deterministically: the same seed always
16
+ renders the same avatar, and different seeds are distinguishable. A `generator`
17
+ config caps each dimension to your brand.
18
+
19
+ ## Generated userpic
20
+
21
+ Pass a `seed` (a stable user id) and a `name`. With no `generator`, each seed
22
+ draws a varied figure: background, head, and body colors, the figure's scale, the
23
+ head and shoulder angles, and the corner radius of each shape all move with the
24
+ seed, while the head and body hues stay coordinated with the background.
25
+
26
+ ```tsx
27
+ <Avatar seed="user-42" name="Vlad Korobov" />
28
+ <Avatar seed="lena-r" name="Lena Rossi" />
29
+ <Avatar seed="8f21ac" name="Sam Okafor" />
30
+ ```
31
+
32
+ ## Initials
33
+
34
+ Turn the shape dimensions off in the `generator` and the avatar falls back to
35
+ initials — the first letter of each of the first three words of `name` — over the
36
+ generated background. The background color still varies with the seed.
37
+
38
+ ```tsx
39
+ const initials = {
40
+ headRadiusTop: { mode: 'off' },
41
+ headRadiusBottom: { mode: 'off' },
42
+ bodyBorderRadius: { mode: 'off' },
43
+ }
44
+
45
+ <Avatar name="Vlad Korobov" generator={initials} />
46
+ <Avatar name="Marie Skłodowska Curie" generator={initials} />
47
+ <Avatar name="Madonna" generator={initials} />
48
+ ```
49
+
50
+ ## Image and badge
51
+
52
+ Set `src` to show an uploaded photo instead of a generated figure, cropped to
53
+ fill the container.
54
+
55
+ `badge` adds a corner badge, colored by tone, on any avatar — image or generated.
56
+ It takes any named tone, so the application decides what each one means:
57
+ `success` for online, `critical` for busy, `warning` for idle, `neutral` for
58
+ offline. Named tones track light and dark mode automatically.
59
+
60
+ The badge sits in a hole masked out of the picture rather than behind a painted
61
+ ring, so whatever is behind the avatar shows through the gap and no ring color
62
+ has to track the surface.
63
+
64
+ ```tsx
65
+ <Avatar round src="/anta-logo.svg" name="Anta" badge="success" />
66
+ <Avatar seed="lena-r" name="Lena Rossi" badge="critical" />
67
+ <Avatar seed="8f21ac" name="Sam Okafor" badge="warning" />
68
+ <Avatar seed="k-tanaka" name="Kenji Tanaka" badge="info" />
69
+ ```
70
+
71
+ `badge` also accepts any literal CSS color for a one-off badge, derived in oklch
72
+ the same way a named tone is.
73
+
74
+ ```tsx
75
+ <Avatar seed="a-lee" name="Ann Lee" badge="#ff1493" />
76
+ <Avatar seed="b-qi" name="Bo Qi" badge="rebeccapurple" />
77
+ ```
78
+
79
+ ## Size
80
+
81
+ `size` is `small` (32px), `medium` (44px, the default), or `large` (64px). Pass a
82
+ number for a pixel size.
83
+
84
+ ```tsx
85
+ <Avatar seed="user-42" name="Vlad Korobov" size="small" />
86
+ <Avatar seed="user-42" name="Vlad Korobov" size="large" />
87
+ <Avatar seed="user-42" name="Vlad Korobov" size={96} />
88
+ ```
89
+
90
+ ## Round
91
+
92
+ `round` makes the frame a circle. Pass a number (px) or a CSS length for a custom
93
+ radius instead. The badge hugs the circle's edge on a round frame.
94
+
95
+ ```tsx
96
+ <Avatar round seed="user-42" name="Vlad Korobov" />
97
+ <Avatar round seed="lena-r" name="Lena Rossi" badge="success" />
98
+ <Avatar seed="k-tanaka" name="Kenji Tanaka" round={10} />
99
+ ```
100
+
101
+ ## Brand constraints
102
+
103
+ The `generator` config caps the space so every avatar sits on your brand. Each
104
+ dimension takes one of four modes: `off` (excluded, a fixed default), `any` (its
105
+ full natural range), `range` (an explicit range), or `list` (a set of values the
106
+ seed picks from). Edit the config in the playground's **Code** tab to try them.
107
+
108
+ **Color ranges** cap OKLCH per channel — lightness, chroma, and hue. Narrow the
109
+ hue to your brand's, keep chroma modest, and every generated color stays in band
110
+ (chroma is clamped into sRGB, so it never clips). `harmony` ties the head and
111
+ body hue to the background.
112
+
113
+ ```tsx
114
+ const brand = {
115
+ bgColor: { mode: 'range', l: [0.5, 0.62], c: [0.09, 0.15], h: [255, 285] },
116
+ harmony: true,
117
+ }
118
+
119
+ <Avatar seed={user.id} name={user.name} generator={brand} />
120
+ ```
121
+
122
+ **Color lists** pass an explicit palette — a scheme the seed picks from at
123
+ random. Combine with `off` shapes for a classic initials avatar restricted to
124
+ your palette.
125
+
126
+ ```tsx
127
+ const palette = {
128
+ bgColor: { mode: 'list', values: ['#6c5ce7', '#00b894', '#e17055', '#0984e3', '#fdcb6e'] },
129
+ headRadiusTop: { mode: 'off' },
130
+ headRadiusBottom: { mode: 'off' },
131
+ bodyBorderRadius: { mode: 'off' },
132
+ }
133
+
134
+ <Avatar name={user.name} generator={palette} />
135
+ ```
136
+
137
+ ### The dimensions
138
+
139
+ | Dimension | Range |
140
+ | --- | --- |
141
+ | `bgColor`, `headColor`, `bodyColor` | OKLCH `l` / `c` / `h` caps, or a palette via `list` |
142
+ | `figureScale` | Relative size of the whole figure |
143
+ | `figureTranslate` | X and Y shift from the centered position |
144
+ | `figureAngle` | Rotation of the whole figure |
145
+ | `headAngle`, `bodyAngle` | Rotation of each part within the figure |
146
+ | `figureGap` | Space between head and body, `0` to `1` of head height |
147
+ | `headRadiusTop`, `headRadiusBottom` | `0` square, `1` fully round, above `1` elongated |
148
+ | `bodyBorderRadius` | `0` square to `1` fully round shoulders |
149
+
150
+ Corner radius is normalized: `0` is a square corner and `1` is fully round, so a
151
+ head with both radii at `1` is a circle. **Above `1` the head elongates** into an
152
+ oval, spending the excess on that half's height — `1.3` on both radii is a tall
153
+ oval, and more at the bottom than the top is an egg.
154
+
155
+ Two rules keep the head reading as a head. The bottom is never rounded less than
156
+ the top, so a value below the top radius is raised to match it: a rounder crown
157
+ over a squarer jaw reads mechanical, and the reverse reads natural. And under
158
+ `any` both radii average near or above `1`, so heads read round to oval unless
159
+ you narrow them yourself.
160
+
161
+ `generator` takes an `AvatarGenConfig`. Every field is optional — anything you
162
+ leave out keeps its default from the table above.
163
+
164
+ ```ts
165
+ interface AvatarGenConfig {
166
+ bgColor?: ColorDim
167
+ headColor?: ColorDim
168
+ bodyColor?: ColorDim
169
+ figureScale?: ScalarDim
170
+ figureTranslate?: Vec2Dim
171
+ figureAngle?: ScalarDim
172
+ headAngle?: ScalarDim
173
+ bodyAngle?: ScalarDim
174
+ figureGap?: ScalarDim
175
+ headRadiusTop?: ScalarDim
176
+ headRadiusBottom?: ScalarDim
177
+ bodyBorderRadius?: ScalarDim
178
+ harmony?: boolean
179
+ }
180
+
181
+ type DimMode = 'off' | 'any' | 'range' | 'list'
182
+ ```
183
+
184
+ ### Modes
185
+
186
+ Every dimension carries a `mode`. It decides which of the dimension's other
187
+ fields are read; the rest are ignored.
188
+
189
+ | `mode` | Behavior | Fields read |
190
+ | --- | --- | --- |
191
+ | `off` | Excluded. The dimension holds a fixed neutral default. | none |
192
+ | `any` | Varies across the dimension's full natural range (the default). | none |
193
+ | `range` | Varies within an explicit range. | `min` / `max`, or `l` / `c` / `h`, or `x` / `y` |
194
+ | `list` | Picks one value at random from an explicit set. | `values` |
195
+
196
+ ### Dimension types
197
+
198
+ **`ScalarDim`** — a single number: scale, an angle, the gap, a corner radius.
199
+
200
+ ```ts
201
+ interface ScalarDim {
202
+ mode?: DimMode
203
+ min?: number // range: lower bound
204
+ max?: number // range: upper bound
205
+ values?: number[] // list: the allowed values
206
+ }
207
+ ```
208
+
209
+ **`ColorDim`** — a color. `range` caps OKLCH per channel as `[min, max]` pairs,
210
+ with chroma clamped into sRGB so a value can never clip. `list` passes a palette
211
+ of any CSS colors.
212
+
213
+ ```ts
214
+ interface ColorDim {
215
+ mode?: DimMode
216
+ l?: [number, number] // range: lightness, 0 to 1
217
+ c?: [number, number] // range: chroma, 0 to ~0.4
218
+ h?: [number, number] // range: hue, 0 to 360
219
+ values?: string[] // list: the allowed CSS colors
220
+ }
221
+ ```
222
+
223
+ **`Vec2Dim`** — the figure's offset from center, used only by `figureTranslate`.
224
+ Each axis runs from −50 to 50 in the frame's coordinates, where 0 centers the
225
+ head.
226
+
227
+ ```ts
228
+ interface Vec2Dim {
229
+ mode?: DimMode // 'list' is not used here
230
+ x?: [number, number] // range: horizontal bounds
231
+ y?: [number, number] // range: vertical bounds
232
+ values?: [number, number][] // list: allowed [x, y] points
233
+ }
234
+ ```
235
+
236
+ `harmony` is a plain boolean rather than a dimension: when true, `headColor` and
237
+ `bodyColor` take their hue from the background so the parts stay coordinated.
238
+
239
+ ### Defaults per dimension
240
+
241
+ What `off` pins each dimension to, and what `any` samples. Corner radii are
242
+ sampled toward the top of their range, so heads average round to oval.
243
+
244
+ | Dimension | `off` | `any` samples |
245
+ | --- | --- | --- |
246
+ | `bgColor` | mid grey | `l` 0.5–0.82, `c` 0.02–0.12, any hue |
247
+ | `headColor` | light grey | `l` 0.78–0.96, `c` 0.015–0.075, any hue |
248
+ | `bodyColor` | light grey | `l` 0.68–0.9, `c` 0.015–0.09, any hue |
249
+ | `figureScale` | `1` | 0.76–1.2 |
250
+ | `figureTranslate` | `[0, 0]` | X −15 to 15; Y −14 to 4 |
251
+ | `figureAngle` | `0` | −15° to 15° |
252
+ | `headAngle` | `0` | −20° to 20° |
253
+ | `bodyAngle` | `0` | −16° to 16° |
254
+ | `figureGap` | `0.25` | 0.12–0.42 |
255
+ | `headRadiusTop` | `1` | 0.35–1.25 |
256
+ | `headRadiusBottom` | `1` | 0.45–1.5 |
257
+ | `bodyBorderRadius` | `1` | 0.25–1 |
258
+ | `harmony` | — | `true` |
259
+
260
+ With every shape dimension (`headRadiusTop`, `headRadiusBottom`,
261
+ `bodyBorderRadius`) set to `off`, no figure shape is configured and the avatar
262
+ renders initials instead.
263
+
264
+ The generation helpers are exported too, for a consumer that needs the result
265
+ outside a component: `resolveAvatar(config, seed)` returns the resolved colors
266
+ and geometry, `avatarToSvg(resolved)` renders it, and `getInitials(name)` derives
267
+ the fallback letters.
268
+
269
+ ### Props
270
+
271
+ | Prop | Type | Default | Description |
272
+ |------|------|---------|-------------|
273
+ | `badge?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | — | Corner badge, colored by tone. Pass a named tone or any literal CSS color;
274
+ the application decides what each tone means (`success` for online,
275
+ `critical` for busy, `neutral` for offline). Omit for no badge. |
276
+ | `generator?` | AvatarGenConfig | — | Brand generation constraints — each dimension is OFF / ANY / RANGE / LIST.
277
+ Colors cap OKLCH ranges or pass an explicit palette. Omit for the default
278
+ varied figure. Define one config for the app and reuse it across avatars. |
279
+ | `name?` | string | — | Person or entity name. Supplies the initials fallback (up to 3 letters from
280
+ the first 3 words) and the accessible name, and seeds generation when `seed`
281
+ is absent. |
282
+ | `round?` | boolean \| number \| string | — | Fully-round (circular) frame. Pass a `number` (px) or a CSS length string
283
+ for a custom radius instead. |
284
+ | `seed?` | string | — | Alphanumeric seed that deterministically drives the generated userpic. The
285
+ same seed always renders the same avatar. Falls back to `name` when omitted,
286
+ so a stable user id or name is a good value. |
287
+ | `size?` | 'small' \| 'medium' \| 'large' \| number | medium | Size of the square container. A number is a pixel size. |
288
+ | `src?` | string | — | Image URL. When set, the image is shown instead of a generated userpic,
289
+ cropped to fill the container. |
290
+
291
+ Use the web component directly when you are not using React or Preact. The
292
+ generation config rides the `config` attribute as a JSON string, so the element
293
+ generates from plain markup with no JavaScript of your own.
294
+
295
+ ```html
296
+ <a-avatar seed="user-42" name="Vlad Korobov"></a-avatar>
297
+
298
+ <a-avatar
299
+ name="Vlad Korobov"
300
+ config='{"headRadiusTop":{"mode":"off"},"headRadiusBottom":{"mode":"off"},"bodyBorderRadius":{"mode":"off"}}'
301
+ ></a-avatar>
302
+ ```
303
+
304
+ Reach for the props first: **`size`** sets the dimensions and **`generator`** the
305
+ generated look. Everything else is `--avatar-*` custom properties on the host and
306
+ `::part()` on the shadow content.
307
+
308
+ The container is a rounded square by default; `round` makes it a circle and takes
309
+ a length for a custom radius, so reach for the prop before the custom property.
310
+ The badge is sized and placed with `--avatar-badge-ratio` (its size as a fraction
311
+ of the frame), `--avatar-badge-inset`, and `--avatar-badge-gap` (the width of the
312
+ cutout around it). Its fill comes from `badge`, so route color through the prop
313
+ rather than overriding `--avatar-badge-color`.
314
+
315
+ ```css
316
+ a-avatar.badge-dot {
317
+ --avatar-badge-ratio: 0.34; /* a larger badge */
318
+ --avatar-badge-gap: 4px; /* in a wider cutout */
319
+ }
320
+ ```
321
+
322
+ The shadow content exposes `::part(frame)` (the picture — the generated figure or
323
+ the image, whichever is showing), `::part(image)` (only the `src` image), and
324
+ `::part(badge)` (the corner badge) for anything the custom properties do not
325
+ cover.
326
+
327
+ ## Example
328
+
329
+ ```tsx
330
+ import { Avatar, type AvatarGenConfig } from '@antadesign/anta'
331
+
332
+ /**
333
+ * @play props Generator
334
+ * The avatar's JSON-like generation settings. Nested values become editable
335
+ * fields in the Props panel.
336
+ */
337
+ const generator = {
338
+ // Every dimension takes one mode: 'off' | 'any' | 'range' | 'list'.
339
+ bgColor: { mode: 'range', l: [0.55, 0.72], c: [0.05, 0.12], h: [0, 360] },
340
+ headColor: { mode: 'any' },
341
+ bodyColor: { mode: 'any' },
342
+ // Corner radius: 0 square, 1 fully round, above 1 elongated into an oval.
343
+ // The bottom is never rounded less than the top, so an egg leans jaw-down.
344
+ headRadiusTop: { mode: 'range', min: 0.5, max: 1.15 },
345
+ headRadiusBottom: { mode: 'range', min: 0.6, max: 1.35 },
346
+ bodyBorderRadius: { mode: 'any' },
347
+ // Space between head and body, as a fraction of head height (0 to 1).
348
+ figureGap: { mode: 'range', min: 0, max: 0.3 },
349
+ figureScale: { mode: 'any' },
350
+ headAngle: { mode: 'any' },
351
+ bodyAngle: { mode: 'any' },
352
+ harmony: true,
353
+ } satisfies AvatarGenConfig
354
+
355
+ function Demo() {
356
+ return (
357
+ /** @play props Avatar */
358
+ <Avatar
359
+ seed="user-42"
360
+ name="Vlad Korobov"
361
+ size={96}
362
+ badge="success"
363
+ round
364
+ generator={generator}
365
+ />
366
+ )
367
+ }
368
+ ```
@@ -0,0 +1,282 @@
1
+ # Box
2
+
3
+ `Box` is a DOM container that adds layout props, overflow states, and browser
4
+ context events.
5
+
6
+ ## Display
7
+
8
+ `display` and `gap` set the matching CSS properties. A numeric `gap` uses
9
+ pixels. A string accepts any CSS length.
10
+
11
+ ```tsx
12
+ <Box round={8}><span /></Box>
13
+ <Box display="flex" round={8} gap={6}><span /></Box>
14
+ <Box display="grid" round={8} gap="0.5rem" style={{ gridTemplateColumns: '1fr 1fr' }}><span /></Box>
15
+ ```
16
+
17
+ `round` accepts the same values. Pass `round` for fully rounded corners. Omit
18
+ it for square corners.
19
+
20
+ `display`, `gap`, and `round` use the `anta.components` layer. Your
21
+ `className` and `style` override them.
22
+
23
+ ## Overflow
24
+
25
+ Box reports when content exceeds its bounds (`overflowX` and `overflowY`), when
26
+ CSS clips it (`clippedX` and `clippedY`), and when it can scroll (`scrollableX`
27
+ and `scrollableY`). It exposes each measurement as a CSS state, so CSS can react
28
+ without JSX state or a measurement ref.
29
+
30
+ A Box measures for `fade`, `onMeasureChange`, or `observe="size"`. Use
31
+ `observe="size"` when CSS reads its states. A Box with none of these attaches no
32
+ observers. Measurement pauses off screen, except `fade`, which measures on
33
+ connection so it can paint its mask.
34
+
35
+ `observe` accepts `"size"`, `"context"`, or `"all"`. A bare `observe` means
36
+ `"all"`. The JSX `Box` derives the needed value from its handlers. When you use
37
+ `a-box` directly, set it yourself; `addEventListener` does not start reporting:
38
+
39
+ ```html
40
+ <a-box observe="size">Content</a-box>
41
+ <a-box observe="all">Content</a-box>
42
+ ```
43
+
44
+ The attribute works with listeners added before upgrade, removed with `once` or
45
+ `AbortSignal`, and replaced during React renders.
46
+
47
+ `.edge` is a demo class name. Use your own selector.
48
+
49
+ ```tsx
50
+ <Box observe="size" round={8} className="edge" style={{ width: 150 }}>Content that fits.</Box>
51
+
52
+ <Box observe="size" round={8} className="edge" style={{ width: 150, overflow: 'hidden', whiteSpace: 'nowrap' }}>
53
+ A label too long for this box.
54
+ </Box>
55
+
56
+ <Box observe="size" round={8} className="edge" style={{ width: 150, height: 56, overflowY: 'auto' }}>
57
+ One. Two. Three. Four. Five. Six. Seven. Eight. Nine. Ten.
58
+ </Box>
59
+ ```
60
+
61
+ ```css
62
+ .edge:state(clipped-x) { border-color: var(--border-3-warning); }
63
+ .edge:state(scrollable-y) { border-color: var(--border-3-info); }
64
+ ```
65
+
66
+ ### Fading a clipped edge
67
+
68
+ `fade` masks each edge that hides content. Scrolling moves the mask to the edge
69
+ that still hides content and removes it at the end. `fadeSize` sets its depth.
70
+
71
+ ```tsx
72
+ <Box fade fadeSize={32} round display="flex" gap={6} style={{ overflowX: 'auto' }}>
73
+ {TAGS.map((t) => <Tag key={t} size="small" label={t} />)}
74
+ </Box>
75
+ ```
76
+
77
+ The mask clips to the padding box. It preserves the Box border, shadows, and
78
+ focus ring when no edge is hidden.
79
+
80
+ The four edge states are `hidden-start-x`, `hidden-end-x`, `hidden-start-y`, and
81
+ `hidden-end-y`. Use `hiddenStartX` and its siblings in JSX. To style the states
82
+ without `fade`, add `observe="size"`.
83
+
84
+ ```css
85
+ /* <Box observe="size" className="my-box"> */
86
+ .my-box:state(hidden-end-x) {
87
+ mask-image: linear-gradient(to right, black calc(100% - 2rem), transparent);
88
+ }
89
+ ```
90
+
91
+ ### Tooltip on clipped content
92
+
93
+ `Tooltip truncatedOnly` uses Box's `isTruncated` getter. It shows when the Box
94
+ clips on either axis, including wrapped children.
95
+
96
+ ```tsx
97
+ const TAGS = ['frontend', 'design-system', 'a11y', 'performance']
98
+
99
+ <Box display="flex" round={8} gap={6} className="tag-box" style={{ height: 34 }}>
100
+ {TAGS.map((t) => <Tag key={t} size="small" label={t} />)}
101
+ <Tooltip truncatedOnly>{TAGS.join(' · ')}</Tooltip>
102
+ </Box>
103
+ ```
104
+
105
+ ```css
106
+ .tag-box { flex-wrap: wrap; padding: 6px; overflow: hidden; }
107
+ ```
108
+
109
+ ## Measurements
110
+
111
+ `measurechange` fires one frame after observation starts and after the Box or its
112
+ content changes. `changed` contains the moved fields, and `current` is the full
113
+ snapshot. The event pauses off screen and reports again when the Box returns.
114
+
115
+ ```tsx title="measurechange"
116
+ <Box
117
+ style={{ maxWidth: 210, overflow: 'hidden', whiteSpace: 'nowrap' }}
118
+ onMeasureChange={(_, { changed, current }) => {
119
+ // changed → { clippedX: true, scrollWidth: 412 }
120
+ if (changed.clippedX) setShowOverflowHint(current.clippedX)
121
+ }}
122
+ >
123
+ Long account name
124
+ </Box>
125
+ ```
126
+
127
+ The preview updates as you resize or scroll the Box. Tags mark `true` values.
128
+
129
+ [`BoxMeasurement`](#boxmeasurement) lists every field. Box maps each overflow
130
+ field to a kebab-case CSS state, such as `overflow-x`, `clipped-y`,
131
+ `scrollable-x`, and `hidden-end-y`. These states are not host attributes. They
132
+ remain current only while `fade`, `observe="size"`, or `onMeasureChange` enables
133
+ measurement.
134
+
135
+ ## Context
136
+
137
+ `contextchange` uses the same `{ changed, current }` shape. Boxes share one
138
+ context observer per window, which stops when the last context reader
139
+ disconnects.
140
+
141
+ [`BoxContext`](#boxcontext) lists every field. `contextchange` sets no CSS
142
+ states. Use `:focus-within`, mode classes, and media queries when CSS is the
143
+ reader.
144
+
145
+ Every value in the following preview is live. Switch the site theme, resize the
146
+ window, zoom the page, or use a touch device. Treat `osVersion` and
147
+ `browserVersion` as hints because browsers freeze parts of these values. Use
148
+ `pointer`, `hover`, or a feature test to gate behavior. The second Box is in a
149
+ `.light` scope, so its `mode` remains `light` on a dark page while `globalMode`
150
+ follows the document.
151
+
152
+ `context.font` is the Box's resolved text style, `context.inset` measures from
153
+ the border edge to the content edge, and `devicePixelRatio` provides the canvas
154
+ scale. Use them to align canvas text with the DOM.
155
+
156
+ `measurement` cannot derive the inset: `width` is the border box, and
157
+ `clientWidth` is the padding box.
158
+
159
+ Set `ctx.font` from `font.shorthand` first. Chromium and Firefox reset
160
+ `fontStretch`, `fontVariantCaps`, `fontKerning`, and `textRendering` when
161
+ `ctx.font` changes. `stretch` and `variantCaps` stay out of the shorthand because
162
+ a percentage `font-stretch` invalidates it.
163
+
164
+ ```tsx title="canvas"
165
+ <Box
166
+ onContextChange={(_, { current }) => {
167
+ const canvas = canvasRef.current
168
+ const ctx = canvas?.getContext('2d')
169
+ if (!ctx) return
170
+
171
+ const { font, devicePixelRatio: dpr } = current
172
+ canvas.width = 300 * dpr
173
+ canvas.height = 80 * dpr
174
+ ctx.scale(dpr, dpr)
175
+
176
+ ctx.font = font.shorthand
177
+ ctx.fontStretch = font.stretch
178
+ ctx.fontVariantCaps = font.variantCaps
179
+ ctx.fontKerning = font.kerning
180
+ ctx.textRendering = font.textRendering
181
+ ctx.letterSpacing = font.letterSpacing
182
+ ctx.wordSpacing = font.wordSpacing
183
+ ctx.direction = font.direction
184
+ ctx.fillStyle = font.color
185
+
186
+ const x = current.inset.borderLeft + current.inset.paddingLeft
187
+ const y = current.inset.borderTop + current.inset.paddingTop
188
+ ctx.fillText('Matches the DOM', x, y + (font.lineHeight ?? font.size))
189
+ }}
190
+ >
191
+ <canvas ref={canvasRef} />
192
+ </Box>
193
+ ```
194
+
195
+ `getComputedStyle(element).font` is empty in Chromium, Firefox, and WebKit, so
196
+ Box assembles the shorthand.
197
+
198
+ ```tsx title="contextchange"
199
+ <Box onContextChange={(_, { changed, current }) => {
200
+ if (changed.mode) updatePreviewTheme(current.mode)
201
+ if (changed.focusWithin) announceFocus(current.focusWithin)
202
+ }}>
203
+ <span>Preview content</span>
204
+ </Box>
205
+ ```
206
+
207
+ ### Props
208
+
209
+ | Prop | Type | Default | Description |
210
+ |------|------|---------|-------------|
211
+ | `display?` | BoxDisplay | block | Layout model for the host. All other layout, sizing, mask, and shadow
212
+ properties stay ordinary `className` / `style` CSS on the Box itself. |
213
+ | `fade?` | boolean | — | Fades out every edge that currently hides clipped content, and drops the
214
+ fade from an edge once the reader scrolls to it. |
215
+ | `fadeSize?` | number \| string | 24 | Depth of the `fade` gradient. A `number` is pixels; a string is any CSS
216
+ length. |
217
+ | `gap?` | number \| string | — | Gap between children, matching the CSS `gap` property. A `number` is
218
+ pixels; a string is any CSS length or two-value gap (`'1rem'`,
219
+ `'8px 16px'`). Applies while the Box is a flex or grid container. |
220
+ | `observe?` | 'size' \| 'context' \| 'all' | — | What the Box watches, when a handler is not what turns it on. `'size'`
221
+ keeps the overflow CSS states (`:state(clipped-x)`, `:state(scrollable-y)`,
222
+ …) current — reach for it when your own CSS is the only reader. `'context'`
223
+ and `'all'` are there for symmetry; passing `onMeasureChange` or
224
+ `onContextChange` already turns the matching half on. |
225
+ | `onContextChange?` | (event, detail) => void | — | Fired after Box's browser and local rendering context changes. `detail`
226
+ contains the changed fields and a full current snapshot. |
227
+ | `onMeasureChange?` | (event, detail) => void | — | Fired after Box geometry or its content-overflow state changes. `detail`
228
+ contains the changed fields and a full current snapshot. |
229
+ | `round?` | boolean \| number \| string | — | Fully-round corners (`border-radius: 999px`, clamped to the box). Pass a
230
+ `number` (px) or a CSS length string (`'1rem'`) for a custom radius. Omit
231
+ for square corners. |
232
+
233
+ ### BoxMeasurement
234
+
235
+ The `measurechange` payload, as `changed` (only what moved) and `current` (the
236
+ whole snapshot).
237
+
238
+ ### BoxContext
239
+
240
+ The `contextchange` payload, in the same two shapes.
241
+
242
+ ### BoxFont
243
+
244
+ `context.font`, the resolved text style.
245
+
246
+ ### BoxInset
247
+
248
+ `context.inset`, the distance from the border edge to the content edge.
249
+
250
+ Use `<a-box>` when you assemble DOM without a JSX wrapper. Both events are
251
+ ordinary, non-bubbling `CustomEvent`s carrying the same detail object.
252
+
253
+ ```html title="a-box"
254
+ <a-box display="grid" gap="8px" round="12px" observe="all" id="summary"
255
+ style="overflow: auto; max-height: 16rem">
256
+ <p>Summary content</p>
257
+ </a-box>
258
+
259
+ <script type="module">
260
+ import '@antadesign/anta/elements/a-box'
261
+
262
+ const box = document.querySelector('#summary')
263
+ box.addEventListener('measurechange', ({ detail }) => {
264
+ console.log(detail.changed, detail.current.scrollableY)
265
+ })
266
+ box.addEventListener('contextchange', ({ detail }) => {
267
+ console.log(detail.changed, detail.current.mode)
268
+ })
269
+ </script>
270
+ ```
271
+
272
+ `gap`, `round`, and `fade-size` read their length through typed `attr()`, which
273
+ today only Chrome supports. The matching custom properties in the host's inline
274
+ style work everywhere and take precedence, so that is what the JSX wrapper sets:
275
+
276
+ ```html
277
+ <a-box gap round fade
278
+ style="--box-gap: 8px; --box-round: 12px; --box-fade-size: 32px">Content</a-box>
279
+ ```
280
+
281
+ `box.measurement`, `box.context`, and `box.isTruncated` read the same values
282
+ synchronously.
package/docs/index.md CHANGED
@@ -21,6 +21,7 @@ Anta is designed for a clean DOM. Prefer the props and attributes exposed by eac
21
21
  - [Tag](./components/tag.md)
22
22
  - [Tooltip](./components/tooltip.md)
23
23
  - [Icon](./components/icon.md)
24
+ - [Avatar](./components/avatar.md)
24
25
  - [Loader](./components/loader.md)
25
26
  - [Progress](./components/progress.md)
26
27
 
@@ -33,9 +34,6 @@ Anta is designed for a clean DOM. Prefer the props and attributes exposed by eac
33
34
  - [Tabs](./components/tabs.md)
34
35
  - [Steps](./components/steps.md)
35
36
 
36
- - [Expander](./components/expander.md)
37
- - [Menu](./components/menu.md)
38
-
39
37
  - [Input](./components/input.md)
40
38
  - [InputAutocomplete](./components/input-autocomplete.md)
41
39
  - [InputDate](./components/input-date.md)
@@ -43,10 +41,13 @@ Anta is designed for a clean DOM. Prefer the props and attributes exposed by eac
43
41
  - [Select](./components/select.md)
44
42
  - [SelectFaceted](./components/select-faceted.md)
45
43
 
44
+ - [Box](./components/box.md)
46
45
  - [Banner](./components/banner.md)
47
46
  - [Card](./components/card.md)
48
47
  - [Dialog](./components/dialog.md)
49
48
  - [Toaster](./components/toaster.md)
49
+ - [Expander](./components/expander.md)
50
+ - [Menu](./components/menu.md)
50
51
 
51
52
  ## Packages
52
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antadesign/anta",
3
- "version": "0.3.25",
3
+ "version": "0.3.26",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",