@antadesign/anta 0.3.26 → 0.3.28

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 (133) hide show
  1. package/README.md +26 -28
  2. package/dist/anta_helpers.d.ts +8 -0
  3. package/dist/anta_helpers.js +18 -0
  4. package/dist/box-types.d.ts +1 -1
  5. package/dist/bundle.css +1 -1
  6. package/dist/bundle.js +36 -42
  7. package/dist/capture-types.d.ts +156 -0
  8. package/dist/components/Banner.js +2 -2
  9. package/dist/components/Box.d.ts +15 -0
  10. package/dist/components/Box.js +3 -11
  11. package/dist/components/Button.js +2 -2
  12. package/dist/components/Capture.d.ts +39 -0
  13. package/dist/components/Capture.js +36 -0
  14. package/dist/components/Card.js +2 -2
  15. package/dist/components/Checkbox.d.ts +10 -11
  16. package/dist/components/Checkbox.js +5 -13
  17. package/dist/components/Expander.js +2 -2
  18. package/dist/components/Input.js +2 -2
  19. package/dist/components/InputTime.js +2 -2
  20. package/dist/components/Loader.js +2 -1
  21. package/dist/components/MenuItem.d.ts +7 -8
  22. package/dist/components/MenuItem.js +4 -4
  23. package/dist/components/Progress.js +2 -2
  24. package/dist/components/RadioGroup.d.ts +12 -13
  25. package/dist/components/RadioGroup.js +7 -23
  26. package/dist/components/Select.d.ts +12 -7
  27. package/dist/components/Select.js +4 -3
  28. package/dist/components/SelectFaceted.d.ts +7 -5
  29. package/dist/components/SelectFaceted.js +4 -3
  30. package/dist/components/Slider.js +3 -3
  31. package/dist/components/Steps.css +1 -1
  32. package/dist/components/Switch.d.ts +7 -8
  33. package/dist/components/Switch.js +3 -11
  34. package/dist/components/Tabs.js +2 -2
  35. package/dist/components/Tag.d.ts +7 -9
  36. package/dist/components/Tag.js +2 -2
  37. package/dist/components/Text.js +2 -2
  38. package/dist/components/Title.js +2 -2
  39. package/dist/components/capture-props.d.ts +30 -0
  40. package/dist/components/capture-props.js +44 -0
  41. package/dist/elements/a-avatar.css +1 -1
  42. package/dist/elements/a-banner.css +1 -1
  43. package/dist/elements/a-box.d.ts +1 -1
  44. package/dist/elements/a-box.js +11 -3
  45. package/dist/elements/a-button.css +1 -1
  46. package/dist/elements/a-button.d.ts +7 -9
  47. package/dist/elements/a-calendar.css +1 -1
  48. package/dist/elements/a-capture.css +1 -0
  49. package/dist/elements/a-capture.d.ts +10 -0
  50. package/dist/elements/a-capture.js +24 -0
  51. package/dist/elements/a-card.css +1 -1
  52. package/dist/elements/a-checkbox.css +1 -1
  53. package/dist/elements/a-checkbox.js +4 -4
  54. package/dist/elements/a-dialog.css +1 -1
  55. package/dist/elements/a-expander.css +1 -1
  56. package/dist/elements/a-expander.js +1 -0
  57. package/dist/elements/a-input-time.css +1 -1
  58. package/dist/elements/a-input-time.js +5 -7
  59. package/dist/elements/a-input.css +1 -1
  60. package/dist/elements/a-input.js +4 -6
  61. package/dist/elements/a-menu-item.css +1 -1
  62. package/dist/elements/a-menu.css +1 -1
  63. package/dist/elements/a-progress.css +1 -1
  64. package/dist/elements/a-radio.css +1 -1
  65. package/dist/elements/a-slider.css +1 -1
  66. package/dist/elements/a-slider.js +6 -9
  67. package/dist/elements/a-switch.css +1 -1
  68. package/dist/elements/a-tab.css +1 -1
  69. package/dist/elements/a-tabs.css +1 -1
  70. package/dist/elements/a-tag.css +1 -1
  71. package/dist/elements/a-text.css +1 -1
  72. package/dist/elements/a-text.js +1 -0
  73. package/dist/elements/a-title.css +1 -1
  74. package/dist/elements/a-tooltip.css +1 -1
  75. package/dist/elements/a-tooltip.d.ts +3 -4
  76. package/dist/elements/a-tooltip.js +0 -1
  77. package/dist/elements/capture-input.d.ts +5 -0
  78. package/dist/elements/capture-input.js +609 -0
  79. package/dist/elements/index.d.ts +1 -0
  80. package/dist/elements/index.js +3 -0
  81. package/dist/general_types.d.ts +56 -23
  82. package/dist/index.d.ts +4 -1
  83. package/dist/index.js +2 -0
  84. package/dist/jsx-runtime.d.ts +2 -1
  85. package/dist/reset.css +1 -1
  86. package/dist/theme-antithesis.css +1 -0
  87. package/dist/theme-antune.css +1 -0
  88. package/dist/tokens.css +1 -1
  89. package/docs/accessibility.md +24 -0
  90. package/docs/changelog.md +79 -0
  91. package/docs/colors.md +141 -0
  92. package/docs/comparison.md +463 -28
  93. package/docs/components/avatar.md +13 -17
  94. package/docs/components/banner.md +13 -27
  95. package/docs/components/box.md +226 -154
  96. package/docs/components/breadcrumbs.md +49 -12
  97. package/docs/components/button.md +48 -37
  98. package/docs/components/capture.md +345 -0
  99. package/docs/components/card.md +17 -38
  100. package/docs/components/checkbox.md +27 -50
  101. package/docs/components/dialog.md +14 -36
  102. package/docs/components/expander.md +15 -40
  103. package/docs/components/icon.md +111 -9
  104. package/docs/components/input-autocomplete.md +9 -11
  105. package/docs/components/input-date.md +33 -23
  106. package/docs/components/input-time.md +15 -23
  107. package/docs/components/input.md +27 -76
  108. package/docs/components/loader.md +7 -5
  109. package/docs/components/menu.md +75 -80
  110. package/docs/components/progress.md +10 -14
  111. package/docs/components/radio.md +28 -46
  112. package/docs/components/select-faceted.md +8 -25
  113. package/docs/components/select.md +33 -105
  114. package/docs/components/slider.md +16 -21
  115. package/docs/components/steps.md +38 -8
  116. package/docs/components/switch.md +16 -19
  117. package/docs/components/tabs.md +52 -40
  118. package/docs/components/tag.md +15 -24
  119. package/docs/components/text.md +10 -23
  120. package/docs/components/title.md +7 -8
  121. package/docs/components/toaster.md +6 -3
  122. package/docs/components/tooltip.md +13 -23
  123. package/docs/index.md +3 -1
  124. package/docs/install-config.md +42 -8
  125. package/docs/normalization.md +48 -3
  126. package/docs/packages/stickers.md +56 -0
  127. package/docs/theming.md +362 -14
  128. package/package.json +7 -4
  129. package/dist/components/InputDate.module.css +0 -1
  130. package/dist/components/Select.module.css +0 -1
  131. package/dist/components/SelectFaceted.module.css +0 -1
  132. package/dist/theme-anta.css +0 -1
  133. /package/dist/{search-index.json → capture-types.js} +0 -0
@@ -196,39 +196,23 @@ applies anything, so a vanilla `<a-banner>` listener can `preventDefault()` a
196
196
  dismiss. The `<Banner>` wrapper's `onDismiss` is notification-only — drive rejection
197
197
  through `dismissed` instead.
198
198
 
199
- ### Props
199
+ ## Component props
200
200
 
201
201
  | Prop | Type | Default | Description |
202
202
  |------|------|---------|-------------|
203
- | `actions?` | ReactNode | — | Trailing controls (buttons, links) rendered as a compact row after the
204
- message and `children`. |
205
- | `align?` | 'start' \| 'center' | 'start' | Positions the content row. `'start'` aligns it to the inline edge; `'center'`
206
- centers it. Wrapped message text follows the selected alignment. |
203
+ | `actions?` | ReactNode | — | Trailing controls (buttons, links) rendered as a compact row after the message and `children`. |
204
+ | `align?` | 'start' \| 'center' | 'start' | Positions the content row. `'start'` aligns it to the inline edge; `'center'` centers it. Wrapped message text follows the selected alignment. |
207
205
  | `children?` | ReactNode | — | Free content placed BETWEEN the message and the actions. |
208
- | `closable?` | boolean | true | Whether the trailing ✕ dismiss button is present. `false` removes it — drive
209
- dismissal yourself (a controlled `dismissed`, or your own control) instead.
210
- It removes the built-in ✕ without making the banner un-dismissible. |
206
+ | `closable?` | boolean | true | Whether the trailing ✕ dismiss button is present. `false` removes it — drive dismissal yourself (a controlled `dismissed`, or your own control) instead. It removes the built-in ✕ without making the banner un-dismissible. |
211
207
  | `defaultDismissed?` | boolean | — | Initial dismissed state for the uncontrolled case (read once on mount). |
212
- | `dismissed?` | boolean | — | Controlled dismissed state. When provided, the consumer owns visibility: the
213
- banner follows this prop only, and clicking ✕ *requests* dismissal (reject by
214
- not updating). **Requires `onDismiss`** — controlled mode never self-hides, so
215
- without a handler to set `dismissed` the ✕ can't close the banner. Leave
216
- undefined for uncontrolled. |
208
+ | `dismissed?` | boolean | — | Controlled dismissed state. When provided, the consumer owns visibility: the banner follows this prop only, and clicking ✕ *requests* dismissal (reject by not updating). **Requires `onDismiss`** — controlled mode never self-hides, so without a handler to set `dismissed` the ✕ can't close the banner. Leave undefined for uncontrolled. |
217
209
  | `message?` | ReactNode | — | Leading message: a string at the banner type scale or any node. |
218
- | `onDismiss?` | () => void | — | Fired when the user dismisses the banner (clicks ✕). Uncontrolled, the banner
219
- hides itself and this notifies. Controlled, use it to accept the
220
- request — set `dismissed` to `true` (or ignore to reject); pair it with
221
- `dismissed`, or the banner can't be closed. |
222
- | `role?` | string | 'status' | ARIA role for the strip host. `'status'` (a polite live region, so the notice
223
- reaches assistive tech) by default; pass `'alert'` for an urgent notice that
224
- should interrupt, or a landmark role. |
225
- | `round?` | boolean \| number \| string | — | Rounded corners for a standalone, floating banner (`border-radius: 999px`,
226
- which clamps to a stadium) instead of the edge-to-edge strip. Still borderless
227
- by default. Pass a `number` (px) or a CSS length string for a custom radius. |
228
- | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Semantic tone, or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`) for
229
- a one-off custom tone. Named tones re-point the surface, text, and border
230
- color (used if you opt into a border); a custom color keeps its hue while
231
- lightness/chroma are pinned. `'neutral'` (the default) is the same as omitting it. |
210
+ | `onDismiss?` | () => void | — | Fired when the user dismisses the banner (clicks ✕). Uncontrolled, the banner hides itself and this notifies. Controlled, use it to accept the request — set `dismissed` to `true` (or ignore to reject); pair it with `dismissed`, or the banner can't be closed. |
211
+ | `role?` | string | 'status' | ARIA role for the strip host. `'status'` (a polite live region, so the notice reaches assistive tech) by default; pass `'alert'` for an urgent notice that should interrupt, or a landmark role. |
212
+ | `round?` | boolean \| number \| string | — | Rounded corners for a standalone, floating banner (`border-radius: 999px`, which clamps to a stadium) instead of the edge-to-edge strip. Still borderless by default. Pass a `number` (px) or a CSS length string for a custom radius. |
213
+ | `tone?` | 'neutral' \| 'brand' \| 'info' \| 'success' \| 'warning' \| 'critical' \| (string & {}) | 'neutral' | Semantic tone, or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`) for a one-off custom tone. Named tones re-point the surface, text, and border color (used if you opt into a border); a custom color keeps its hue while lightness/chroma are pinned. `'neutral'` (the default) is the same as omitting it. |
214
+
215
+ ## Web Component
232
216
 
233
217
  Use the web component directly when you are not using React or Preact and a native control does not fit.
234
218
 
@@ -246,6 +230,8 @@ Slots hold the message, actions, and close control. A close button dispatches
246
230
  </a-banner>
247
231
  ```
248
232
 
233
+ ## Styling
234
+
249
235
  Reach for the props first: **`tone`** sets the color (any CSS color for a custom
250
236
  tone — it derives the surface, text, and border color in oklch), **`round`** the
251
237
  corners. The one knob worth keeping is **`--banner-tone-source`** — the color a
@@ -1,12 +1,12 @@
1
1
  # Box
2
2
 
3
- `Box` is a DOM container that adds layout props, overflow states, and browser
4
- context events.
3
+ `Box` is a DOM container with layout props, overflow states, and browser context
4
+ events. For custom wheel, pointer, and touch handling, use [Capture](./capture.md).
5
5
 
6
6
  ## Display
7
7
 
8
- `display` and `gap` set the matching CSS properties. A numeric `gap` uses
9
- pixels. A string accepts any CSS length.
8
+ Use `display`, `gap`, and `round` for layout and corners. Numeric lengths use
9
+ pixels; strings accept CSS lengths. A bare `round` fully rounds the corners.
10
10
 
11
11
  ```tsx
12
12
  <Box round={8}><span /></Box>
@@ -14,35 +14,12 @@ pixels. A string accepts any CSS length.
14
14
  <Box display="grid" round={8} gap="0.5rem" style={{ gridTemplateColumns: '1fr 1fr' }}><span /></Box>
15
15
  ```
16
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
17
  ## Overflow
24
18
 
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.
19
+ Box reports overflow, clipping, and scrollability as measurements and CSS
20
+ states. Use `observe="size"` to keep CSS states current. `fade` and
21
+ `onMeasureChange` also enable measurement; without these, Box adds no size
22
+ observers.
46
23
 
47
24
  `.edge` is a demo class name. Use your own selector.
48
25
 
@@ -65,33 +42,36 @@ The attribute works with listeners added before upgrade, removed with `once` or
65
42
 
66
43
  ### Fading a clipped edge
67
44
 
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.
45
+ `fade` masks edges with hidden content and removes the mask as scrolling reveals
46
+ them. `fadeSize` sets its depth. It measures on connection, even off screen.
70
47
 
71
48
  ```tsx
72
- <Box fade fadeSize={32} round display="flex" gap={6} style={{ overflowX: 'auto' }}>
49
+ const TAGS = ['frontend', 'design-system', 'a11y', 'performance']
50
+
51
+ <Box fade fadeSize={32} round display="flex" gap={6} className="fade-demo" style={{ width: 190, overflowX: 'auto' }}>
73
52
  {TAGS.map((t) => <Tag key={t} size="small" label={t} />)}
74
53
  </Box>
54
+
55
+ <Box fade fadeSize={32} round={8} className="fade-demo" style={{ width: 190, height: 56, overflowY: 'auto' }}>
56
+ One. Two. Three. Four. Five. Six. Seven. Eight. Nine. Ten. Eleven. Twelve.
57
+ </Box>
58
+ ```
59
+
60
+ ```css
61
+ .fade-demo { flex-wrap: nowrap; padding: 10px; border: 1px solid var(--border-4); }
62
+ .fade-demo a-tag { flex: 0 0 auto; }
75
63
  ```
76
64
 
77
65
  The mask clips to the padding box. It preserves the Box border, shadows, and
78
66
  focus ring when no edge is hidden.
79
67
 
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
- ```
68
+ To style hidden edges without `fade`, use `observe="size"` and the
69
+ `hidden-start-x`, `hidden-end-x`, `hidden-start-y`, or `hidden-end-y` CSS states.
90
70
 
91
71
  ### Tooltip on clipped content
92
72
 
93
- `Tooltip truncatedOnly` uses Box's `isTruncated` getter. It shows when the Box
94
- clips on either axis, including wrapped children.
73
+ `Tooltip truncatedOnly` shows when Box clips content on either axis, including
74
+ wrapped children.
95
75
 
96
76
  ```tsx
97
77
  const TAGS = ['frontend', 'design-system', 'a11y', 'performance']
@@ -108,175 +88,267 @@ const TAGS = ['frontend', 'design-system', 'a11y', 'performance']
108
88
 
109
89
  ## Measurements
110
90
 
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.
91
+ `onMeasureChange` reports one frame after observation starts and when the Box or
92
+ its content changes. `changed` contains changed fields; `current` is the full
93
+ snapshot. Reporting pauses off screen and resumes when Box returns.
94
+
95
+ Resize or scroll the Box to update its measurements.
114
96
 
115
97
  ```tsx title="measurechange"
98
+ const [measurement, setMeasurement] = useState<BoxMeasurement | null>(null)
99
+
116
100
  <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
- }}
101
+ round={8}
102
+ className="measure-probe-box"
103
+ onMeasureChange={(_, { current }) => setMeasurement(current)}
122
104
  >
123
- Long account name
105
+ <Text size="small" priority="tertiary">Resize or scroll this Box.</Text>
106
+ <div className="measure-probe-wide">wide content, so both axes overflow</div>
107
+ <div className="measure-probe-wide">and a second line, so the vertical axis does too</div>
108
+ <div className="measure-probe-wide">and a third</div>
124
109
  </Box>
110
+
111
+ <div className="measure-probe-readout">
112
+ {Object.entries(measurement ?? {}).map(([field, value]) => (
113
+ <Tag key={field} size="small" label={field} value={String(value)} />
114
+ ))}
115
+ </div>
125
116
  ```
126
117
 
127
- The preview updates as you resize or scroll the Box. Tags mark `true` values.
118
+ ```css
119
+ .measure-probe-box {
120
+ resize: both;
121
+ overflow: auto;
122
+ inline-size: 320px;
123
+ block-size: 132px;
124
+ min-inline-size: 120px;
125
+ min-block-size: 64px;
126
+ padding: 10px;
127
+ border: 1px solid var(--border-4);
128
+ }
129
+ .measure-probe-wide { inline-size: 520px; padding-block: 6px; }
130
+ .measure-probe-readout { display: flex; flex-wrap: wrap; gap: 6px; }
131
+ ```
128
132
 
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.
133
+ See the [measurement fields](#boxmeasurement). Overflow fields map to kebab-case
134
+ CSS states, such as `clippedX` → `:state(clipped-x)`.
134
135
 
135
136
  ## Context
136
137
 
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.
138
+ `onContextChange` reports theme, focus, and browser context as
139
+ `{ changed, current }`. See the [context fields](#boxcontext). For CSS, use
140
+ `:focus-within`, mode classes, and media queries instead.
141
+
142
+ Switch themes, resize, or zoom to update the preview. The `.light` Box keeps its
143
+ local `mode`; `globalMode` follows the document. Browser and OS versions may be
144
+ frozen, so use `pointer`, `hover`, or feature tests to choose behavior.
140
145
 
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.
146
+ ```tsx title="contextchange"
147
+ import { useState } from 'react'
148
+ import { Box, Tag, Text, type BoxContext } from '@antadesign/anta'
149
+
150
+ function ScopedContext() {
151
+ const [context, setContext] = useState<BoxContext | null>(null)
152
+
153
+ return (
154
+ <div className="light">
155
+ <Box display="flex" round={8} gap={6} className="context-probe-box"
156
+ onContextChange={(_, { current }) => setContext(current)}>
157
+ <Text size="small" priority="tertiary">inside a .light scope</Text>
158
+ <Tag size="small" tone="brand" label="mode" value={context?.mode ?? '…'} />
159
+ <Tag size="small" label="globalMode" value={context?.globalMode ?? '…'} />
160
+ </Box>
161
+ </div>
162
+ )
163
+ }
164
+ ```
165
+
166
+ ```css
167
+ .context-probe-box {
168
+ flex-wrap: wrap;
169
+ align-items: center;
170
+ padding: 10px;
171
+ border: 1px solid var(--border-4);
172
+ background: var(--bg-1);
173
+ color: var(--text-1);
174
+ }
175
+ ```
144
176
 
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.
177
+ <a id="canvas"></a>
151
178
 
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.
179
+ ### Canvas-related styles
155
180
 
156
- `measurement` cannot derive the inset: `width` is the border box, and
157
- `clientWidth` is the padding box.
181
+ Use `context.font` and `devicePixelRatio` to match canvas text to the DOM.
182
+ The first line is DOM text; the second is drawn on canvas. Switch themes or
183
+ zoom to update both. `context.inset` also reports border and padding when you
184
+ need to align drawing coordinates with the Box's content edge.
158
185
 
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.
186
+ Set `ctx.font` before spacing and direction. Box assembles `font.shorthand`
187
+ because `getComputedStyle(element).font` can be empty. Stretch and variant caps
188
+ remain separate fields; percentage stretch is invalid in the shorthand.
163
189
 
164
190
  ```tsx title="canvas"
165
- <Box
191
+ const canvasRef = useRef<HTMLCanvasElement>(null)
192
+
193
+ <Box className="canvas-probe"
166
194
  onContextChange={(_, { current }) => {
167
195
  const canvas = canvasRef.current
168
196
  const ctx = canvas?.getContext('2d')
169
- if (!ctx) return
197
+ if (!canvas || !ctx) return
170
198
 
171
199
  const { font, devicePixelRatio: dpr } = current
172
- canvas.width = 300 * dpr
173
- canvas.height = 80 * dpr
200
+ canvas.width = canvas.clientWidth * dpr
201
+ canvas.height = canvas.clientHeight * dpr
174
202
  ctx.scale(dpr, dpr)
175
203
 
176
204
  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
205
  ctx.letterSpacing = font.letterSpacing
182
206
  ctx.wordSpacing = font.wordSpacing
183
- ctx.direction = font.direction
207
+ ctx.direction = font.direction === 'rtl' ? 'rtl' : 'ltr'
184
208
  ctx.fillStyle = font.color
185
209
 
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))
210
+ ctx.fillText('Matches the DOM', 0, font.lineHeight ?? font.size)
189
211
  }}
190
212
  >
191
- <canvas ref={canvasRef} />
213
+ <span>Matches the DOM</span>
214
+ <canvas ref={canvasRef} aria-label="Canvas text using the Box font and color" />
192
215
  </Box>
193
216
  ```
194
217
 
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>
218
+ ```css
219
+ .canvas-probe { width: 100%; font-size: 20px; line-height: 32px; color: var(--text-1); }
220
+ .canvas-probe canvas { display: block; width: 100%; height: 48px; }
205
221
  ```
206
222
 
207
- ### Props
223
+ ## Component props
208
224
 
209
225
  | Prop | Type | Default | Description |
210
226
  |------|------|---------|-------------|
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. |
227
+ | `display?` | BoxDisplay | block | Layout model for the host. All other layout, sizing, mask, and shadow properties stay ordinary `className` / `style` CSS on the Box itself. |
228
+ | `fade?` | boolean | — | Fades out every edge that currently hides clipped content, and drops the fade from an edge once the reader scrolls to it. |
229
+ | `fadeSize?` | number \| string | 24 | Depth of the `fade` gradient. A `number` is pixels; a string is any CSS length. |
230
+ | `gap?` | number \| string | — | Gap between children, matching the CSS `gap` property. A `number` is pixels; a string is any CSS length or two-value gap (`'1rem'`, `'8px 16px'`). Applies while the Box is a flex or grid container. |
231
+ | `observe?` | 'size' \| 'context' \| 'all' | — | What the Box watches, when a handler is not what turns it on. `'size'` keeps the overflow CSS states (`:state(clipped-x)`, `:state(scrollable-y)`, …) current — reach for it when your own CSS is the only reader. `'context'` and `'all'` are there for symmetry; passing `onMeasureChange` or `onContextChange` already turns the matching half on. |
232
+ | `onContextChange?` | (event, detail) => void | — | Fired after Box's browser and local rendering context changes. `detail` contains the changed fields and a full current snapshot. |
233
+ | `onMeasureChange?` | (event, detail) => void | — | Fired after Box geometry or its content-overflow state changes. `detail` contains the changed fields and a full current snapshot. |
234
+ | `round?` | boolean \| number \| string | — | Fully-round corners (`border-radius: 999px`, clamped to the box). Pass a `number` (px) or a CSS length string (`'1rem'`) for a custom radius. Omit for square corners. |
232
235
 
233
236
  ### BoxMeasurement
234
237
 
235
- The `measurechange` payload, as `changed` (only what moved) and `current` (the
236
- whole snapshot).
238
+ | Field | Type | Default | Description |
239
+ |------|------|---------|-------------|
240
+ | `clientHeight` | number | — | |
241
+ | `clientWidth` | number | — | Padding-box dimensions, matching the browser's `clientWidth` / `clientHeight`. |
242
+ | `clippedX` | boolean | — | The exceeded content is visually clipped on this axis. |
243
+ | `clippedY` | boolean | — | |
244
+ | `height` | number | — | |
245
+ | `hiddenEndX` | boolean | — | |
246
+ | `hiddenEndY` | boolean | — | |
247
+ | `hiddenStartX` | boolean | — | Clipped content sits past this specific edge, in logical writing-mode terms. A clipped box that has not been scrolled hides content past its end edge only; scroll it to the end and the hidden content moves to the start. These drive the `fade` mask. |
248
+ | `hiddenStartY` | boolean | — | |
249
+ | `overflowX` | boolean | — | Content exceeds the padding box on this axis, regardless of CSS overflow. |
250
+ | `overflowY` | boolean | — | |
251
+ | `scrollableX` | boolean | — | The exceeded content can be scrolled by the reader on this axis. |
252
+ | `scrollableY` | boolean | — | |
253
+ | `scrollHeight` | number | — | |
254
+ | `scrollLeft` | number | — | Current scroll offset, matching `scrollLeft` / `scrollTop`. |
255
+ | `scrollTop` | number | — | |
256
+ | `scrollWidth` | number | — | Full scrollable-content dimensions, matching `scrollWidth` / `scrollHeight`. |
257
+ | `width` | number | — | Border-box width and height in CSS pixels. |
237
258
 
238
259
  ### BoxContext
239
260
 
240
- The `contextchange` payload, in the same two shapes.
261
+ | Field | Type | Default | Description |
262
+ |------|------|---------|-------------|
263
+ | `backgroundColor` | string | — | Resolved `background-color`. Needed when the box's content is drawn somewhere else — an offscreen canvas, a worker, an export — where the box's own background is not behind it. |
264
+ | `browser` | BoxBrowser | — | Browser family. |
265
+ | `browserVersion` | number | — | Browser major version, or `0` when unknown. Minor and patch digits are frozen by every engine, so only the major number is reported. |
266
+ | `devicePixelRatio` | number | — | `window.devicePixelRatio`: CSS pixels per device pixel. `1` on a standard display, `2` on most Retina screens, and a fraction under OS or browser zoom. Live — it re-reports on zoom and when the window moves to a monitor with a different density. |
267
+ | `focusWithin` | boolean | — | Whether focus is on the box or any of its descendants, read from the native `:focus-within`. For CSS, use that pseudo-class directly; this field is for logic that cannot query the DOM. |
268
+ | `font` | BoxFont | — | Resolved text style, ready to hand to a canvas 2D context. |
269
+ | `globalMode` | BoxMode | — | Mode on `<html>`, independent of an enclosing local scope. |
270
+ | `hover` | boolean | — | Whether ordinary hover interaction is available. |
271
+ | `inset` | BoxInset | — | Padding and border widths, for placing content inside the border box. |
272
+ | `mobile` | boolean | — | Whether the browser reports a mobile device. |
273
+ | `mode` | BoxMode | — | Closest scoped Anta mode. A local `.light` can override a dark document. |
274
+ | `os` | BoxOS | — | Operating-system family. |
275
+ | `osVersion` | number | — | Operating-system major version, or `0` when the browser withholds it. Browsers freeze this: every engine reports macOS as `10.15.7` and Windows 11 as `10.0`, so only Android and iOS carry a real number. Treat it as a hint, never as a gate. |
276
+ | `pointer` | BoxPointer | — | Most precise available primary pointer. |
277
+ | `reducedMotion` | boolean | — | Whether the reader asks for reduced motion. |
278
+ | `systemAppearance` | BoxMode | — | Browser / operating-system color preference, independent of Anta classes. |
241
279
 
242
280
  ### BoxFont
243
281
 
244
282
  `context.font`, the resolved text style.
245
283
 
284
+ | Field | Type | Default | Description |
285
+ |------|------|---------|-------------|
286
+ | `color` | string | — | Resolved text color. Canvas: `ctx.fillStyle`. |
287
+ | `direction` | string | — | Canvas: `ctx.direction`. |
288
+ | `family` | string | — | Resolved family list, quoted as the engine reports it. |
289
+ | `featureSettings` | string | — | Canvas 2D consumes neither of these. They are here for text you measure or draw some other way. |
290
+ | `kerning` | string | — | Canvas: `ctx.fontKerning`. |
291
+ | `letterSpacing` | string | — | A length, never `normal` - `normal` is reported as `0px`, which is what `ctx.letterSpacing` accepts. |
292
+ | `lineHeight` | number \| null | — | Line height in CSS pixels, or `null` when it computes to `normal`. Canvas ignores line height in `ctx.font`; this is for laying text out yourself. |
293
+ | `shorthand` | string | — | CSS `font` shorthand, assembled here because every engine returns an empty string for the computed shorthand. `stretch` and `variantCaps` are left out of it deliberately: a percentage `font-stretch` makes every engine reject the whole string and fall back to `10px sans-serif`. Apply those through `ctx.fontStretch` / `ctx.fontVariantCaps` after setting `ctx.font`. |
294
+ | `size` | number | — | Font size in CSS pixels. |
295
+ | `stretch` | string | — | Computed `font-stretch`, a percentage such as `88%`. Canvas: `ctx.fontStretch`. |
296
+ | `style` | string | — | `normal`, `italic`, or an `oblique <angle>`. |
297
+ | `textRendering` | string | — | Canvas: `ctx.textRendering`, which WebKit does not implement. |
298
+ | `variantCaps` | string | — | Canvas: `ctx.fontVariantCaps`. |
299
+ | `variationSettings` | string | — | |
300
+ | `weight` | number | — | Numeric weight, 1-1000. |
301
+ | `wordSpacing` | string | — | Same normalization as `letterSpacing`. Canvas: `ctx.wordSpacing`. |
302
+
246
303
  ### BoxInset
247
304
 
248
305
  `context.inset`, the distance from the border edge to the content edge.
249
306
 
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.
307
+ | Field | Type | Default | Description |
308
+ |------|------|---------|-------------|
309
+ | `borderBottom` | number | — | |
310
+ | `borderLeft` | number | — | |
311
+ | `borderRight` | number | — | |
312
+ | `borderTop` | number | — | |
313
+ | `paddingBottom` | number | — | |
314
+ | `paddingLeft` | number | — | |
315
+ | `paddingRight` | number | — | |
316
+ | `paddingTop` | number | — | |
317
+
318
+ ## Web component
319
+
320
+ Use `<a-box>` without JSX. Events are non-bubbling `CustomEvent`s with the same
321
+ `detail`. Set `observe="size"`, `"context"`, or `"all"` explicitly; adding a
322
+ listener does not enable observation. A bare `observe` means `"all"`.
252
323
 
253
324
  ```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>
325
+ <a-box display="grid" gap="8px" round="12px" observe="all"
326
+ style="--box-gap: 8px; --box-round: 12px; padding: 12px; border: 1px solid var(--border-4)">
327
+ <span>Summary content</span>
328
+ <span>Observation enabled</span>
257
329
  </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
330
  ```
271
331
 
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:
332
+ Import `@antadesign/anta/elements/a-box` to register the element. Listen for
333
+ `measurechange` and `contextchange` with `addEventListener`.
334
+ `box.measurement`, `box.context`, and `box.isTruncated` read values synchronously.
335
+
336
+ ## Styling
337
+
338
+ Your `className` and `style` override `display`, `gap`, and `round` through the
339
+ `anta.components` CSS layer.
340
+
341
+ Without typed CSS `attr()` support, raw `gap`, `round`, and `fade-size` length
342
+ attributes need matching custom properties. The JSX wrapper sets these for you:
275
343
 
276
344
  ```html
277
- <a-box gap round fade
278
- style="--box-gap: 8px; --box-round: 12px; --box-fade-size: 32px">Content</a-box>
345
+ <a-box display="flex" gap round fade class="raw-box"
346
+ style="--box-gap: 8px; --box-round: 12px; --box-fade-size: 32px">
347
+ <span>Layout</span><span>Context</span><span>Measurements</span>
348
+ </a-box>
279
349
  ```
280
350
 
281
- `box.measurement`, `box.context`, and `box.isTruncated` read the same values
282
- synchronously.
351
+ ```css
352
+ .raw-box { width: 190px; overflow: auto; padding: 12px; border: 1px solid var(--border-4); }
353
+ .raw-box span { flex: 0 0 auto; }
354
+ ```