@acronis-platform/ui-react 4.1.0 → 5.1.0

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 (79) hide show
  1. package/dist/components/ui/area-chart/area-chart.js +191 -85
  2. package/dist/components/ui/area-chart/area-chart.js.map +1 -1
  3. package/dist/components/ui/bar-chart/bar-chart.js +288 -225
  4. package/dist/components/ui/bar-chart/bar-chart.js.map +1 -1
  5. package/dist/components/ui/card-widget-carousel/card-widget-carousel.js +85 -0
  6. package/dist/components/ui/card-widget-carousel/card-widget-carousel.js.map +1 -0
  7. package/dist/components/ui/card-widget-carousel/card-widget.js +82 -0
  8. package/dist/components/ui/card-widget-carousel/card-widget.js.map +1 -0
  9. package/dist/components/ui/category-bar/category-bar.js.map +1 -1
  10. package/dist/components/ui/chart/chart-format.js +25 -22
  11. package/dist/components/ui/chart/chart-format.js.map +1 -1
  12. package/dist/components/ui/chart/chart-palette.js +9 -8
  13. package/dist/components/ui/chart/chart-palette.js.map +1 -1
  14. package/dist/components/ui/chart/chart.js +127 -89
  15. package/dist/components/ui/chart/chart.js.map +1 -1
  16. package/dist/components/ui/funnel-chart/funnel-chart.js +189 -113
  17. package/dist/components/ui/funnel-chart/funnel-chart.js.map +1 -1
  18. package/dist/components/ui/input-select/input-select.js +18 -9
  19. package/dist/components/ui/input-select/input-select.js.map +1 -1
  20. package/dist/components/ui/line-chart/line-chart.js +194 -86
  21. package/dist/components/ui/line-chart/line-chart.js.map +1 -1
  22. package/dist/components/ui/radar-chart/radar-chart.js +76 -66
  23. package/dist/components/ui/radar-chart/radar-chart.js.map +1 -1
  24. package/dist/components/ui/scatter-chart/scatter-chart.js +5 -2
  25. package/dist/components/ui/scatter-chart/scatter-chart.js.map +1 -1
  26. package/dist/components/ui/separator/separator.js +52 -10
  27. package/dist/components/ui/separator/separator.js.map +1 -1
  28. package/dist/components/ui/sidebar-secondary/sidebar-secondary.js +144 -121
  29. package/dist/components/ui/sidebar-secondary/sidebar-secondary.js.map +1 -1
  30. package/dist/components/ui/tree-item/tree-item.js +67 -0
  31. package/dist/components/ui/tree-item/tree-item.js.map +1 -0
  32. package/dist/components/ui/treemap/treemap.js +73 -66
  33. package/dist/components/ui/treemap/treemap.js.map +1 -1
  34. package/dist/index.js +109 -107
  35. package/dist/llms/area-chart.md +2 -1
  36. package/dist/llms/bar-chart.md +132 -33
  37. package/dist/llms/card-widget-carousel.md +252 -0
  38. package/dist/llms/category-bar.md +2 -2
  39. package/dist/llms/funnel-chart.md +269 -101
  40. package/dist/llms/input-select.md +175 -1
  41. package/dist/llms/line-chart.md +2 -1
  42. package/dist/llms/metric.md +10 -5
  43. package/dist/llms/radar-chart.md +15 -5
  44. package/dist/llms/scatter-chart.md +2 -1
  45. package/dist/llms/separator.md +27 -8
  46. package/dist/llms/sidebar-secondary.md +235 -4
  47. package/dist/llms/tree-item.md +359 -0
  48. package/dist/llms/treemap.md +47 -36
  49. package/dist/llms.txt +5 -4
  50. package/dist/react.js +109 -107
  51. package/dist/src/components/ui/area-chart/area-chart.d.ts +10 -0
  52. package/dist/src/components/ui/bar-chart/bar-chart.d.ts +75 -10
  53. package/dist/src/components/ui/bar-chart/bar-chart.docs.d.ts +146 -0
  54. package/dist/src/components/ui/bar-chart/index.d.ts +1 -1
  55. package/dist/src/components/ui/card-widget-carousel/card-widget-carousel.d.ts +9 -0
  56. package/dist/src/components/ui/card-widget-carousel/card-widget.d.ts +43 -0
  57. package/dist/src/components/ui/card-widget-carousel/index.d.ts +2 -0
  58. package/dist/src/components/ui/chart/chart-format.d.ts +9 -1
  59. package/dist/src/components/ui/chart/chart-palette.d.ts +38 -10
  60. package/dist/src/components/ui/chart/chart.d.ts +8 -1
  61. package/dist/src/components/ui/chart/index.d.ts +2 -2
  62. package/dist/src/components/ui/funnel-chart/funnel-chart.d.ts +92 -26
  63. package/dist/src/components/ui/funnel-chart/index.d.ts +1 -1
  64. package/dist/src/components/ui/input-select/input-select.d.ts +34 -2
  65. package/dist/src/components/ui/line-chart/line-chart.d.ts +11 -1
  66. package/dist/src/components/ui/radar-chart/radar-chart.d.ts +42 -9
  67. package/dist/src/components/ui/separator/separator.d.ts +9 -1
  68. package/dist/src/components/ui/sidebar-secondary/sidebar-secondary.d.ts +23 -3
  69. package/dist/src/components/ui/tree-item/index.d.ts +1 -0
  70. package/dist/src/components/ui/tree-item/tree-item.d.ts +64 -0
  71. package/dist/src/components/ui/treemap/treemap.d.ts +16 -5
  72. package/dist/src/index.d.ts +2 -1
  73. package/dist/ui-react.css +1 -1
  74. package/package.json +1 -1
  75. package/dist/components/ui/meter/meter.js +0 -79
  76. package/dist/components/ui/meter/meter.js.map +0 -1
  77. package/dist/llms/meter.md +0 -177
  78. package/dist/src/components/ui/meter/index.d.ts +0 -1
  79. package/dist/src/components/ui/meter/meter.d.ts +0 -25
@@ -45,7 +45,10 @@ The next-gen select **field**: a label (with an optional required marker), a tri
45
45
  box showing the value / placeholder and a chevron, an optional description or error,
46
46
  and a floating dropdown with an in-dropdown search, sections, single- or
47
47
  multiple-selectable items, and loading / empty / error status. It composes Base UI
48
- `Select` and the `--ui-input-select-*` token tier.
48
+ `Select` and the `--ui-input-select-*` token tier. The dropdown's placement is
49
+ controllable (`side` / `align` / offsets / collision avoidance, or an explicit
50
+ `anchor` element to position against instead of the trigger), and
51
+ `is-popover-styled` swaps its chrome to the popover look — see `behavior.md`.
49
52
 
50
53
  > `Select` is an alias of this family (re-pointed off the deleted `--ui-form-*` tier).
51
54
  > Prefer importing `InputSelect*` directly in new code.
@@ -151,6 +154,14 @@ use it inside a custom tree/item renderer to filter items the flat
151
154
  | `name` | `string` | — | no | Form field name submitted with the form (via a hidden input). |
152
155
  | `placeholder` | `string` | — | no | Hint shown by the value part when nothing is selected. |
153
156
  | `items` | `Record<string, string>` | — | no | Map of value → label for every selectable option, so the value part can render a selected option's label while the popup is closed without inspecting the rendered items. |
157
+ | `side` | `'top' | 'bottom' | 'left' | 'right' | 'inline-start' | 'inline-end'` | `'bottom'` | no | On the content — which side of the trigger the popup renders on. |
158
+ | `align` | `'start' | 'center' | 'end'` | `'start'` | no | On the content — alignment along the chosen side. |
159
+ | `side-offset` | `number` | `4` | no | On the content — distance in px from the trigger. |
160
+ | `align-offset` | `number` | `0` | no | On the content — offset in px along the alignment axis (perpendicular to `side`), to nudge the popup along the trigger's edge without changing its alignment. |
161
+ | `collision-avoidance` | `{ side?: 'flip' | 'shift' | 'none'; align?: 'flip' | 'shift' | 'none'; fallbackAxisSide?: 'start' | 'end' | 'none' }` | `the platform default (flip/shift)` | no | On the content — collision-avoidance strategy. Pass `{ side: 'none', fallbackAxisSide: 'none' }` alongside an explicit `side`/`align` to lock the popup to that placement with no auto-flip fallback. |
162
+ | `anchor` | `Element | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | null` | `the trigger element` | no | On the content — element (or ref/getter) the popup positions itself against, overriding the default of anchoring to the trigger. Use when the visible trigger isn't the element tracked internally — e.g. an external button drives a hidden trigger, and the popup should align with the button instead. |
163
+ | `portal-container` | `Element | ShadowRoot | null | RefObject<Element | ShadowRoot | null>` | `nearest PortalContainerProvider, or document.body` | no | On the content — container to portal the dropdown into. Pass an element to scope the portal (e.g. a shadow root, so the popup inherits styles defined there). |
164
+ | `is-popover-styled` | `boolean` | `false` | no | On the content — render the dropdown chrome like `PopoverContent` (fade/zoom/slide animation, no shadow) instead of the default static-shadow dropdown chrome. |
154
165
 
155
166
  ## Events
156
167
 
@@ -229,6 +240,112 @@ border.
229
240
  **Then** it shows the matching icon and message (and, for `error`, an optional retry
230
241
  action).
231
242
 
243
+ ### Popup placement and chrome
244
+
245
+ **Given** no positioning props on the content
246
+ **When** the popup opens
247
+ **Then** it is placed against the **trigger**, below it and aligned to its start
248
+ edge, and the platform's own collision handling may flip or shift it to keep it in
249
+ view. `side` / `align` / `side-offset` / `align-offset` / `collision-avoidance`
250
+ each override one part of that; passing `collision-avoidance` with no fallbacks
251
+ alongside an explicit `side` pins the popup to that placement even when it
252
+ overflows.
253
+
254
+ **Given** an `anchor` on the content
255
+ **When** the popup opens
256
+ **Then** it positions itself against **that** element instead of the trigger,
257
+ decoupling where the popup appears from where the trigger sits in layout. This is
258
+ for the case where the element the user actually operates is not the trigger — an
259
+ external button drives the open state and the trigger stays mounted but visually
260
+ hidden — so the popup should align with the button, not with the hidden trigger's
261
+ position in flow. The trigger still owns the combobox semantics either way (see
262
+ `accessibility.md`).
263
+
264
+ **Given** an external button that drives a controlled `open` and is the popup's
265
+ `anchor`
266
+ **When** the user presses that button while the popup is open
267
+ **Then** the platform reports the press as an **outside press** close before the
268
+ button's own activation handler runs, so the consumer must cancel that specific
269
+ close (`open-change` with `reason = outside-press` and the event target inside
270
+ the button → `cancel()`) for the button to act as a toggle; otherwise the
271
+ activation handler reopens the popup and it can never be dismissed from the
272
+ button. A press outside both the button and the popup still closes it.
273
+
274
+ **Given** an external button that drives a controlled `open`, with the real
275
+ trigger kept mounted but visually hidden (screen-reader-only, not removed)
276
+ **When** the popup closes via Escape or by selecting an item
277
+ **Then** at the moment the close is reported focus is **still inside the popup
278
+ that is about to unmount**, and the platform can move it to **its own trigger** —
279
+ which is invisible — only **afterwards**, asynchronously. Neither path reports
280
+ the close with focus already on the trigger. So the consumer must (a) mark the
281
+ trigger `tab-index = -1` so it is never reached by Tab, and (b) reclaim focus for
282
+ their visible external control themselves once the close is committed,
283
+ **re-asserted once the platform's own focus-restore attempts have settled**. On
284
+ Escape, a gate that recognizes "focus is still inside the closing popup" matches
285
+ at the moment of the synchronous reclaim and moves focus to the external control
286
+ before the platform's own move happens, so nothing overwrites it afterwards; the
287
+ deferred re-assertion then finds the external control already focused and is a
288
+ no-op. On item selection, the same synchronous reclaim also lands, but the
289
+ platform's async move to the hidden trigger can still happen afterwards and pull
290
+ focus back onto it — so the deferred re-assertion is load-bearing on this path,
291
+ not a no-op, and reclaims focus a second time once that move has settled. Doing
292
+ only one of (a)/(b) is a WCAG 2.4.7 failure: `tab-index = -1` does not block the
293
+ platform's programmatic focus call, and the manual reclaim alone leaves an
294
+ invisible control in the tab order. The reclaim belongs after the state commit
295
+ in the same `open-change` handler, so a close the consumer cancels (see below)
296
+ does not move focus.
297
+
298
+ **Given** the same visually hidden trigger
299
+ **When** the popup closes because of an outside press onto **nothing focusable**
300
+ (a blank area of the page)
301
+ **Then** the platform performs **no** focus restore at all — focus is simply lost
302
+ to the document body. The consumer's own reclaim is the only thing that recovers
303
+ it, so it must cover this case too, not just the trigger-focused one.
304
+
305
+ **Given** the same visually hidden trigger
306
+ **When** the popup closes because of an outside press onto **another focusable
307
+ element** (an input elsewhere on the page)
308
+ **Then** focus must end up on **that** element, not on the external button — the
309
+ user deliberately focused it and pulling focus back is a bug. The platform
310
+ reports this close **once** (reason = outside press), while focus is still inside
311
+ the popup, so a gate keyed on "focus is unclaimed" (still inside the closing
312
+ popup, on the hidden trigger, or on the document body) **does** match on that
313
+ single report and does issue its focus call. The correct end state comes from the
314
+ platform's own native focus move to the pressed element happening **after** that
315
+ synchronous call and overriding it — not from the gate recognizing and skipping
316
+ this case. What the gate does guarantee is that the **deferred** re-assertion
317
+ cannot undo that: by the time it runs the pressed element holds focus, which is
318
+ not an unclaimed state, so it is a no-op. An unconditional focus return on close
319
+ is therefore still wrong — it would fire again after the platform's move and
320
+ steal focus back.
321
+
322
+ **Given** a cancelled close (the consumer called `cancel()` on `open-change`)
323
+ **When** the popup therefore stays open
324
+ **Then** the in-dropdown search query is **preserved** — a cancelled close does
325
+ not silently clear what the user typed. (A consumer that leaves the close
326
+ un-cancelled but simply doesn't act on it still gets the query reset; cancelling
327
+ is what marks the close as not taking effect.)
328
+
329
+ **Given** an external button that drives a controlled `open` and closes the
330
+ popup directly (its own handler sets `open = false`, so the platform never
331
+ reports the transition through `open-change`)
332
+ **When** the popup is reopened
333
+ **Then** the in-dropdown search query is **empty** — the reset follows the
334
+ `open` prop's `true → false` transition as well as `open-change`, so a
335
+ button-driven close cannot carry a stale query into the next open.
336
+
337
+ **Given** `is-popover-styled`
338
+ **When** the popup opens
339
+ **Then** its container chrome is drawn from the **popover** tokens rather than the
340
+ dropdown ones (fill / border / corner radius), the dropdown's static shadow is
341
+ dropped, and open/close is animated (fade + zoom, sliding in from the resolved
342
+ `side`) instead of appearing instantly. Reach for it when the dropdown is acting
343
+ as a floating menu that should read in the same visual language as `Popover` —
344
+ a tenant/entity picker opened from a page control, not a plain form field's
345
+ option list. It changes only the container chrome and the enter/exit transition:
346
+ the popup still sizes to the anchor width, keeps the dropdown's vertical padding,
347
+ and every row inside (search, sections, items, status) is unchanged.
348
+
232
349
  ### Interaction
233
350
 
234
351
  **Given** the trigger
@@ -264,6 +381,63 @@ action).
264
381
  Arrow-key roaming inside the listbox skips it entirely — it cannot be
265
382
  reached with the keyboard once the popup is open, only by pointer. It
266
383
  remains a normal native tab stop outside that roving focus order.
384
+ - **Decoupled anchor:** `anchor` moves only where the popup is _drawn_ — the
385
+ combobox semantics (`aria-expanded`, `aria-controls`, the accessible name) and
386
+ the focus return on close stay on the **trigger**, and cannot be relocated. So
387
+ if the visible control is a separate element (an external button opening a
388
+ visually hidden trigger), that button needs its own accessible name (visible
389
+ text or `aria-label`) and, because it is the control the user actually
390
+ operates, its own `aria-expanded` bound to the same controlled `open` state
391
+ that drives the popup. `aria-controls` on the button is optional and only
392
+ wirable by giving `InputSelectContent` an explicit `id` and referencing it
393
+ while the popup is open. Keep the trigger reachable — do not `display: none`
394
+ it, or closing returns focus to nothing. Prefer opening from the real trigger
395
+ whenever the design allows it.
396
+ - **Visually hidden trigger — focus is the consumer's job (2.4.7):** leaving the
397
+ trigger merely `sr-only`-reachable is **not** sufficient. It is still
398
+ focusable while invisible, and on the close paths where the platform restores
399
+ focus to it (Escape, selecting an item) keyboard focus lands on a control the
400
+ user cannot see — a WCAG 2.4.7 Focus Visible failure. On other close paths
401
+ (an outside press onto nothing focusable) the platform does not restore focus
402
+ at all and it is simply lost to the document body — also a failure. The
403
+ required pattern is **both** of:
404
+ 1. `tabIndex={-1}` on the **trigger** (`InputSelectTrigger`) — not on the
405
+ `InputSelectField` wrapper, which is a plain non-focusable element — so the
406
+ invisible trigger is out of the natural Tab sequence;
407
+ 2. a **conditional** focus return to the consumer's own visible control on
408
+ close (`onOpenChange` → after committing the new state, `if (!nextOpen)`
409
+ reclaim focus for the external button). Two properties of the platform make
410
+ this non-trivial. First, **the platform's own focus restore may still be
411
+ pending when the close is reported** — on the Escape and item-selection
412
+ paths focus is still inside the popup that is about to unmount at handler
413
+ time, and the move to the (invisible) trigger can land afterwards. On
414
+ Escape a synchronous reclaim already lands on the visible control before
415
+ that move happens, so nothing overwrites it; on item selection the
416
+ platform's move can still land on the hidden trigger after a synchronous
417
+ reclaim, so the reclaim must be re-asserted once that settles. Second,
418
+ **on some close reasons the platform's restore does not happen at all** —
419
+ an outside press onto nothing focusable leaves focus on the document
420
+ body. So the focus return must be **re-asserted once the platform's own
421
+ focus-restore attempts have settled**, not only synchronously during the
422
+ handler — even where the synchronous call already suffices, the deferred
423
+ re-assertion is a harmless no-op.
424
+ Crucially, the reclaim must **never override an element the user has
425
+ legitimately focused since** — an outside press onto another focusable
426
+ control is that user's choice of focus, and an unconditional refocus steals
427
+ it. Gate the reclaim on focus being _unclaimed_: still inside the closing
428
+ popup, on the hidden trigger, or lost to the document body. Anything else
429
+ is left alone.
430
+ `tabIndex={-1}` alone does not stop the platform's programmatic `.focus()`
431
+ call, and the reclaim alone leaves the invisible trigger in the Tab order —
432
+ each fixes one half. The reclaim must sit after the state commit, so the
433
+ cancelled `outside-press` branch (which returns early) does not move focus
434
+ for a close that never happened.
435
+ - **External-button toggle:** an external button must also cancel the
436
+ `outside-press` close that its own pointerdown triggers (`onOpenChange` →
437
+ `eventDetails.reason === 'outside-press'` and the event target inside the
438
+ button → `eventDetails.cancel()`), otherwise the popup cannot be dismissed
439
+ from the button at all — a keyboard/AT user who activates the button a second
440
+ time gets a popup that reopens instead of closing. Escape still closes it.
267
441
  - **Selection indicator:** single-select shows a trailing check on the selected item;
268
442
  multiple-select shows a leading checkbox per item (not focusable — the row is the
269
443
  control).
@@ -108,12 +108,13 @@ across brands until the real data-viz palette lands.
108
108
  | `yAxisLabel` | `string` | — | no | Title rendered beside the vertical (Y) axis (rotated). |
109
109
  | `yUnit` | `string` | — | no | Unit suffix appended to Y-axis tick values (recharts `unit`); the X axis is categorical. |
110
110
  | `strokeWidth` | `number` | `2` | no | Stroke width of each line. |
111
- | `showDots` | `boolean` | `true` | no | Render a dot at each data point. |
111
+ | `showDots` | `boolean` | `false` | no | Render a dot at each data point. |
112
112
  | `dotSize` | `number` | `3` | no | Radius of each point's dot, in px. Its hover dot is 2px larger. |
113
113
  | `showActiveDot` | `boolean` | — | no | Enlarge the hovered point's dot. Defaults to following `showDots`, so set it explicitly for a hover-only line (dots off, active dot on) or a static-only line (dots on, active dot off). |
114
114
  | `lineSettings` | `Record<string, { color?: string; strokeWidth?: number; dashed?: boolean; curveType?: CurveType; showDots?: boolean; dotSize?: number; showLabel?: boolean; labelPosition?: LabelPosition }>` | — | no | Per-series style override keyed by `dataKeys` entry — anything unset falls back to the chart-wide prop, so an entry states only what differs. A series listed in `comparisonKeys` keeps its dashed, dimmed, dot-less treatment: `color` / `strokeWidth` / `curveType` still apply to it, but `showDots` / `dotSize` do not. |
115
115
  | `referenceLine` | `Line | Line[] where Line = { value?: number; average?: boolean | string; label?: string; labelPosition?: 'top' | 'insideTopLeft' | 'insideTopRight' | 'insideBottomLeft' | 'insideBottomRight' }` | — | no | One or more dashed reference/average lines on the value axis. Pass a single object or an array. Per line, a fixed `value` wins; otherwise the mean of one series (`average: '<key>'`) or of every plotted series (`average: true`, which includes any `comparisonKeys` overlay). An optional `label` captions each, at the rule's top right unless `labelPosition` moves it away from a series it would sit on. The same config shape as `BarChart` / `AreaChart`. |
116
116
  | `connectNulls` | `boolean` | `false` | no | Bridge `null` gaps in the data instead of breaking the line. |
117
+ | `projectionStart` | `string | number` | — | no | The category value (matching an `xKey` entry) at which the projection zone starts. Ticks from this point onward render in the disabled text color. The actual series are clipped to the left of the boundary; a dashed, dot-less projection continuation renders on the right. Comparison series (`comparisonKeys`) are excluded from the projection split and render unclipped across the full width. |
117
118
  | `showGrid` | `boolean` | `true` | no | Render the CartesianGrid. |
118
119
  | `showTooltip` | `boolean` | `true` | no | Render the hover tooltip. |
119
120
  | `showLegend` | `boolean` | `true` | no | Render the legend. |
@@ -57,7 +57,7 @@ GROSS MARGIN [Last 30 days] ← label + caption
57
57
  ## Composition
58
58
 
59
59
  Metric is a Card. Put the trend in the `trend` slot (a `TrendIndicator`), a
60
- timeframe in `caption` (a `Tag`), and drop a chart / `Meter` breakdown /
60
+ timeframe in `caption` (a `Tag`), and drop a chart / labelled-bar breakdown /
61
61
  `Separator` / insight line into `children`:
62
62
 
63
63
  ```tsx
@@ -76,11 +76,16 @@ timeframe in `caption` (a `Tag`), and drop a chart / `Meter` breakdown /
76
76
  />
77
77
  }
78
78
  >
79
- <Meter
80
- label="Healthy"
81
- value={46}
79
+ <BarChart
80
+ orientation="horizontal"
81
+ items={[
82
+ {
83
+ label: 'Healthy',
84
+ value: 46,
85
+ color: 'var(--ui-background-status-strong-success)',
86
+ },
87
+ ]}
82
88
  max={54}
83
- color="var(--ui-background-status-strong-success)"
84
89
  />
85
90
  <Separator className="my-3" />
86
91
  <p>+3 customers predicted at-risk within 30 days.</p>
@@ -98,8 +98,8 @@ across brands until the real data-viz palette lands.
98
98
  | `seriesSettings` | `Record<string, RadarChartSeriesSettings>` | — | no | Per-series style overrides keyed by a `dataKeys` entry — `color`, `stroke`, `fillOpacity`, `strokeWidth`, `dot`, `dotRadius`, `activeDot`. Unplotted keys are ignored. An overridden `color` — or `stroke`, which the renderer reads first — carries into that series' legend swatch and tooltip dot. |
99
99
  | `fillOpacity` | `number` | `0.3` | no | Fill opacity of each radar area. |
100
100
  | `strokeWidth` | `number` | `2` | no | Stroke width of each radar outline. |
101
- | `showDots` | `boolean` | `false` | no | Render a dot at each axis point. |
102
- | `dotRadius` | `number` | `3` | no | Radius of the per-point dots when `showDots` is on. |
101
+ | `showDots` | `boolean` | `true` | no | Render a dot at each axis point. Visible by default to match the Figma chart. |
102
+ | `dotRadius` | `number` | `2` | no | Radius of the per-point dots when `showDots` is on. |
103
103
  | `activeDot` | `boolean` | `true` | no | Enlarge the hovered point's dot. On by default; `false` for a chart that should not react to the pointer. |
104
104
  | `showGrid` | `boolean` | `true` | no | Render the polar grid (the web). |
105
105
  | `radialLines` | `boolean` | `true` | no | Draw the grid's spokes (the radial lines out to each category). Off leaves only the concentric rings. |
@@ -117,15 +117,14 @@ across brands until the real data-viz palette lands.
117
117
  | `radiusAxisTickCount` | `number` | `5` | no | Desired number of ticks on the value scale (a hint, not exact). Also sets how many rings the grid draws, whether or not the scale is shown. |
118
118
  | `radiusAxisReversed` | `boolean` | `false` | no | Invert the value scale (maximum at the centre) for a metric where less is better. |
119
119
  | `cx` | `number | string` | `'50%'` | no | Horizontal centre of the web, in px or a percentage of the width. |
120
- | `cy` | `number | string` | `'50%'` | no | Vertical centre of the web, in px or a percentage of the height. |
120
+ | `cy` | `number | string` | `the middle of the plot band` | no | Vertical centre of the web, in px or a percentage of the height. Defaults to the middle of the plot band — 50% of what the legend leaves, not of the whole box — because the underlying chart centres a polar plot on the whole surface and takes no account of the legend band beneath it. |
121
121
  | `startAngle` | `number` | `90` | no | Angle the first category sits at, in degrees. |
122
122
  | `endAngle` | `number` | `-270` | no | Angle the sweep ends at, in degrees — a full clockwise turn from `startAngle` by default. |
123
123
  | `innerRadius` | `number | string` | `0` | no | Radius of the hole at the centre, in px or a percentage of the available radius. Lifts the grid and the areas off the centre. |
124
- | `outerRadius` | `number | string` | `'80%'` | no | Outer radius of the web, in px or a percentage of the available radius. |
124
+ | `outerRadius` | `number | string` | `78` | no | Outer radius of the web, in px or a percentage of the available radius. Defaults to 78px to match the Figma geometry; the legend and labels do not change the radius. |
125
125
  | `margin` | `{ top?: number; right?: number; bottom?: number; left?: number }` | — | no | Plot-area margin, in px. Passing the object replaces all four sides; a side left out is `0`, not the default `5`, so name every side. |
126
126
  | `showTooltip` | `boolean` | `true` | no | Render the hover tooltip. |
127
127
  | `showLegend` | `boolean` | `true` | no | Render the legend. |
128
- | `legendPosition` | `'top' | 'bottom'` | `'bottom'` | no | Which side of the chart the legend sits on. |
129
128
  | `tooltipContent` | `ReactElement | ((props) => ReactNode)` | — | no | Replace the default tooltip. Pass a configured ChartTooltipContent (from this library) — e.g. with a formatter / labelFormatter — to customize the tooltip content without composing recharts. Ignored when showTooltip is false. |
130
129
  | `animate` | `boolean` | `false` | no | Enable entrance animation. Off by default so output/baselines are unchanged. |
131
130
  | `animationDuration` | `number` | — | no | Animation duration in ms. |
@@ -231,6 +230,17 @@ Scenario: Geometry
231
230
  Then the web, its axes, and the areas are all laid out to match
232
231
  ```
233
232
 
233
+ ```gherkin
234
+ Scenario: The box holds the category labels
235
+ Given a fixed outerRadius
236
+ Then the chart reserves a plot band tall enough for the outer category labels
237
+ — the outer radius, the tick size, and half a line of tick text on each side
238
+ And showLabels, which widens the tick gap, widens that band with it
239
+ And the web is centred on the plot band rather than on the whole box, so the
240
+ bottom label clears the legend and the top one clears the upper edge
241
+ And a caller-supplied height or cy wins over both
242
+ ```
243
+
234
244
  ```gherkin
235
245
  Scenario: Tooltip on hover
236
246
  Given showTooltip is true
@@ -106,6 +106,7 @@ across brands until the real data-viz palette lands.
106
106
  |------|------|---------|----------|-------------|
107
107
  | `series` | `ScatterSeries[]` | — | yes | One `<Scatter>` per entry, each `{ key, data }` with its own point array. `key` matches a `config` entry and drives its `--color-<key>` fill. A single entry gives an ungrouped scatter. |
108
108
  | `config` | `ChartConfig` | — | yes | Series take their colour from the container's `palette`; each entry, keyed by a `series[].key`, maps that series to a `label` and an optional `tone`. Resolved into the `--color-<key>` custom properties the points fill from. |
109
+ | `palette` | `ChartPalette` | `{ type: 'diverging', pair: 'teal-violet' }` | no | The dataviz palette this chart's series are painted from. Defaults to diverging teal-violet, matching the Figma reference design. |
109
110
  | `xKey` | `string` | — | yes | Numeric field for the horizontal axis. |
110
111
  | `yKey` | `string` | — | yes | Numeric field for the vertical axis. |
111
112
  | `xAxisLabel` | `string` | — | no | Title rendered beneath the horizontal (X) axis. |
@@ -127,7 +128,7 @@ across brands until the real data-viz palette lands.
127
128
  | `xAxisInterval` | `number | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd' | 'equidistantPreserveStart'` | — | no | X-axis tick density — a recharts placement strategy, or a fixed number of ticks to skip between two rendered ones (recharts interval: 0 shows every tick, 1 every other one, 2 every third). |
128
129
  | `yAxisTickCount` | `number` | — | no | Desired number of ticks on the value axis (recharts tickCount; a hint, not exact). Applies to whichever axis holds the values — Y for most charts, X for BarChart with orientation="horizontal". |
129
130
  | `yAxisDomain` | `'auto' | 'dataMin-dataMax' | 'zero'` | — | no | Value-axis domain preset — auto (fit the data at both ends; the axis need not include 0), zero (anchor the axis at 0, which is also recharts' behaviour when the prop is omitted), or dataMin-dataMax (tight to the data, no padding). Applies to whichever axis holds the values — Y for most charts, X for BarChart with orientation="horizontal". |
130
- | `gridDashed` | `boolean` | — | no | Draw grid lines dashed instead of solid. |
131
+ | `gridDashed` | `boolean` | `true` | no | Draw grid lines dashed instead of solid. |
131
132
  | `gridHorizontal` | `boolean` | — | no | Show horizontal grid lines (defaults to the chart's own default when unset). |
132
133
  | `gridVertical` | `boolean` | — | no | Show vertical grid lines (defaults to the chart's own default when unset). |
133
134
  | `animate` | `boolean` | `false` | no | Enable entrance animation. Off by default so output/baselines are unchanged. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  > A thin rule that visually and semantically divides content. Built on the Base UI Separator primitive (sets the `separator` role + aria-orientation). Ported from the legacy shadcn-uikit `Separator`; uses the shared divider token.
4
4
 
5
- **Status:** draft · **Category:** layout · **Since:** v0.15.0
5
+ **Status:** ready · **Category:** layout · **Since:** v0.15.0
6
6
 
7
7
  ## Import
8
8
 
@@ -15,14 +15,15 @@ import { Separator } from '@acronis-platform/ui-react'
15
15
  ```tsx
16
16
  <Separator className="my-4" />
17
17
  <Separator orientation="vertical" />
18
+ <Separator size="S2" />
18
19
  ```
19
20
 
20
21
  A thin rule that divides content, horizontally or vertically.
21
22
 
22
- > **Status: draft (design-pending v1).** Ported from the legacy
23
- > `@acronis-platform/shadcn-uikit` `Separator`; uses the shared `bg-border`
24
- > divider token (no `--ui-separator-*` tier). Reconcile with
25
- > `/figma-component Separator <url> --update` once a mockup lands.
23
+ > Ported from the legacy `@acronis-platform/shadcn-uikit` `Separator`; matched
24
+ > to Figma's `DividerHorizontal` component set (node 788:15147). Uses the
25
+ > shared `--ui-border-on-surface-divider` token directly (no `--ui-separator-*`
26
+ > tier).
26
27
 
27
28
  ## When to use
28
29
 
@@ -46,6 +47,9 @@ import { Separator } from '@acronis-platform/ui-react';
46
47
  <Separator orientation="vertical" />
47
48
  <span>Recovery</span>
48
49
  </div>
50
+
51
+ {/* size (S1/S2/S3, Figma default S1) applies built-in surrounding spacing */}
52
+ <Separator size="S2" />
49
53
  ```
50
54
 
51
55
  ## Props
@@ -53,11 +57,12 @@ import { Separator } from '@acronis-platform/ui-react';
53
57
  | Prop | Type | Default | Required | Description |
54
58
  |------|------|---------|----------|-------------|
55
59
  | `orientation` | `'horizontal' | 'vertical'` | `'horizontal'` | no | Direction of the rule; sets aria-orientation and swaps the 1px dimension. |
60
+ | `size` | `'S1' | 'S2' | 'S3'` | `'S1'` | no | Surrounding spacing baked into the rule (Figma variant `Size`). `S1` (default) has none; `S2`/`S3` apply `--ui-gap-4`/`--ui-gap-8` as margin on the axis perpendicular to the line. |
56
61
 
57
62
  ## Behavior
58
63
 
59
- Separator is a static divider. It has no props beyond `orientation`, no state,
60
- and no interaction.
64
+ Separator is a static divider. It has no state and no interaction; its only
65
+ props are `orientation` and `size`.
61
66
 
62
67
  ```gherkin
63
68
  Scenario: Horizontal rule (default)
@@ -72,6 +77,20 @@ Scenario: Vertical rule
72
77
  And it needs a sized (height-bearing) flex/inline context to be visible
73
78
  ```
74
79
 
80
+ ```gherkin
81
+ Scenario: Default size has no built-in spacing
82
+ Given a Separator with no size
83
+ Then it renders with size="S1" and applies no surrounding margin
84
+ ```
85
+
86
+ ```gherkin
87
+ Scenario: S2/S3 apply built-in surrounding spacing
88
+ Given a Separator with size="S2" or size="S3"
89
+ Then it applies --ui-gap-4 or --ui-gap-8 as margin on the axis
90
+ perpendicular to the rule (vertical margin for horizontal, horizontal
91
+ margin for vertical)
92
+ ```
93
+
75
94
  ## Accessibility
76
95
 
77
96
  - Renders `role="separator"` (Base UI), with `aria-orientation` reflecting the
@@ -83,5 +102,5 @@ Scenario: Vertical rule
83
102
 
84
103
  ## Contrast
85
104
 
86
- The line uses `--ui-border-on-surface-border`, which meets non-text contrast
105
+ The line uses `--ui-border-on-surface-divider`, which meets non-text contrast
87
106
  against the page surface in light and dark themes.