@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
@@ -67,7 +67,7 @@ breadcrumb rail.
67
67
  | `SidebarSecondaryMenuSubContent` | `Collapsible.Panel` `<div><ul>` | Level-2 child list |
68
68
  | `SidebarSecondaryMenuSubItem` | `<li><a>` | Level-2 indented leaf |
69
69
  | `SidebarSecondaryMenuItemExtras` | `<span>` | Trailing shortcut / external-link / tag |
70
- | `SidebarSecondaryCollapseTrigger` | `<li><button>` | Footer "Collapse menu" button; toggles `expanded` |
70
+ | `SidebarSecondaryCollapseTrigger` | `<li><button>` | Footer "Collapse menu" button; toggles `expanded` (`disabled` when not collapsible) |
71
71
 
72
72
  ## Quick Examples
73
73
 
@@ -149,6 +149,44 @@ Author both the `SidebarSecondaryContent` (section list) and the
149
149
  `expanded` state via CSS — both stay in the DOM, so the breadcrumb is
150
150
  SSR-present and no JS branch is needed.
151
151
 
152
+ ## Locking the collapse state
153
+
154
+ `collapsible` defaults to `true`. Pass `collapsible={false}` when the surface
155
+ must not let the user change the panel's expanded/collapsed state. It locks that
156
+ toggle only — **not the width**, which the user can still change by dragging or
157
+ Arrow-shrinking while the panel is expanded. `resizable={false}` is what locks
158
+ the width value itself; the two props are independent and compose:
159
+
160
+ ```tsx
161
+ <SidebarSecondary collapsible={false}>{/* … */}</SidebarSecondary>
162
+ ```
163
+
164
+ - Every user-initiated collapse/expand path is blocked — resize-edge click,
165
+ double-click-to-expand, drag past the collapse threshold, the resize edge's
166
+ Arrow/Enter/Space/Home keys, and the footer
167
+ `SidebarSecondaryCollapseTrigger` (which renders natively `disabled` and
168
+ shows no `expandTooltip`).
169
+ - **Resizing still works while the panel is expanded.** A drag or Arrow-shrink
170
+ that would have collapsed the panel clamps to the minimum width instead. Pass
171
+ `resizable={false}` as well to lock the width outright.
172
+ - It does **not** force `expanded`. `collapsible={false}` with
173
+ `defaultExpanded={false}` is valid and renders a permanently collapsed rail; a
174
+ controlled `expanded` prop still has full authority. On that permanently
175
+ collapsed rail the resize edge stays focusable and keeps its accessible name,
176
+ but it is inert — drag, click and the Arrow keys do nothing observable, and
177
+ only `Home` (or a double-click) still writes the default width, invisibly.
178
+ - Every tooltip is adjusted for you, so the copy never advertises an inert
179
+ gesture. The footer collapse trigger's `expandTooltip` is suppressed outright
180
+ (a disabled control cannot expand anything; an explicit value does not bring
181
+ it back). The two resize-edge defaults narrow: **Expanded**, the default drops
182
+ the
183
+ "Collapse: Click" line and reads "Resize: Drag" / "Reset size: Double click".
184
+ **Collapsed**, the default narrows to "Reset size: Double click" alone —
185
+ dragging and clicking a permanently collapsed rail do nothing, but a
186
+ double-click still resets the width. An explicit `resizeTooltipExpanded` /
187
+ `resizeTooltipCollapsed` value always wins, including `null` to suppress the
188
+ tooltip entirely.
189
+
152
190
  ## Spec Files
153
191
 
154
192
  | File | Contents |
@@ -177,10 +215,11 @@ SSR-present and no JS branch is needed.
177
215
  | `actions` | `ReactNode` | `undefined` | no | Trailing header actions on the section-label (e.g. a ghost ButtonIcon), rendered outside the toggle so they stay independently operable. |
178
216
  | `unreadRollup` | `ReactNode` | `undefined` | no | A rollup badge (e.g. a Tag with an unread count) shown in an expandable section's header only while it is collapsed. |
179
217
  | `resizable` | `boolean` | `true` | no | Enable the draggable resize edge on the right border. When `true`, the sidebar can be resized between the expanded-width token and twice that value. |
218
+ | `collapsible` | `boolean` | `true` | no | Allow the user to collapse/expand the panel (resize-edge click, drag past the collapse threshold, keyboard, and the footer collapse trigger). When `false` those interactions never change `expanded` and the footer collapse trigger renders natively disabled with no `aria-expanded`; while the panel is expanded, dragging still resizes it, clamped to the minimum width. Independent of `expanded` — pair it with `defaultExpanded: false` for a permanently collapsed rail, on which the resize edge stays focusable but is inert apart from the (invisible) width reset, which `Home` and a double-click on the edge both still perform. |
180
219
  | `resizeAriaLabel` | `string` | `'Resize sidebar'` | no | Accessible label for the resize edge (`role="separator"`). Consumers should localize this string for i18n. |
181
- | `resizeTooltipExpanded` | `ReactNode` | `(English tooltip: Resize/Collapse/Reset)` | no | Tooltip content shown when the sidebar is expanded. Pass `null` to hide the tooltip entirely. Consumers should localize for i18n. |
182
- | `resizeTooltipCollapsed` | `ReactNode` | `(English tooltip: Resize/Expand)` | no | Tooltip content shown when the sidebar is collapsed. Pass `null` to hide the tooltip entirely. Consumers should localize for i18n. |
183
- | `expandTooltip` | `ReactNode` | `'Expand'` | no | Tooltip text shown on the collapse trigger when the sidebar is collapsed. Consumers should localize this string for i18n. |
220
+ | `resizeTooltipExpanded` | `ReactNode` | `(English tooltip: Resize/Collapse/Reset; Resize/Reset when not collapsible)` | no | Tooltip content shown when the sidebar is expanded. The default depends on `collapsible`: with `collapsible: false` it drops the "Collapse: Click" line, since clicking the edge can no longer collapse the panel. Pass `null` to hide the tooltip entirely. Consumers should localize for i18n. |
221
+ | `resizeTooltipCollapsed` | `ReactNode` | `(English tooltip: Resize/Expand; 'Reset size: Double click' only when not collapsible)` | no | Tooltip content shown when the sidebar is collapsed. The default depends on `collapsible`: with `collapsible: false` it narrows to the single "Reset size: Double click" line, because dragging and clicking a permanently collapsed rail have no effect while a double-click still resets the width. Pass `null` to hide the tooltip entirely, or an explicit node to replace it. Consumers should localize for i18n. |
222
+ | `expandTooltip` | `ReactNode` | `'Expand'` | no | Tooltip text shown on the collapse trigger when the sidebar is collapsed. Suppressed entirely with `collapsible: false` — the trigger is disabled then, so the copy would advertise an action that cannot run. An explicit value does not override that suppression. Consumers should localize this string for i18n. |
184
223
  | `extras-variant` | `'tag' | 'externalLink' | 'shortcut' | 'tag-externalLink'` | — | yes | Which trailing affordance the extras part renders. React: the `variant` prop on `SidebarSecondaryMenuItemExtras`. (Named here without the bare `variant` key reserved for a root cva axis.) |
185
224
  | `shortcut` | `string` | `undefined` | no | Keyboard-shortcut text for the extras `shortcut` variant. |
186
225
  | `tag` | `ReactNode` | `undefined` | no | Tag content for the extras `tag` / `tag-externalLink` variants. |
@@ -256,6 +295,111 @@ SSR-present and no JS branch is needed.
256
295
  > DOM; visibility is toggled purely by `data-[state]` selectors (SSR-safe, no JS
257
296
  > branch).
258
297
 
298
+ #### `collapsible={false}` blocks every user collapse/expand path
299
+
300
+ **Given** a SidebarSecondary with `collapsible={false}`
301
+ **When** the user clicks the resize edge, double-clicks it while collapsed,
302
+ drags it past the collapse threshold, presses the shrink Arrow at the minimum
303
+ width, presses the grow Arrow while collapsed, presses Enter/Space on the resize
304
+ edge, presses Home while collapsed, or activates the footer
305
+ `SidebarSecondaryCollapseTrigger`
306
+ **Then** `expanded` never changes and `data-state` stays as it was
307
+ **And** the footer collapse trigger renders natively `disabled`
308
+
309
+ > **The width reset is not inert while collapsed.** Blocking the expand does not
310
+ > block the width reset: on the resize edge of a `collapsible={false}`, collapsed
311
+ > panel, **`Home` and a double-click** both still reset the stored width to
312
+ > `defaultWidth` — neither write is gated on `collapsible` (in
313
+ > `handleDoubleClick`, only its `toggleExpanded()` call is). `onWidthChange`
314
+ > fires **only when `defaultWidth` differs from the current stored width**:
315
+ > `setWidth` returns early on `next === currentWidth`, so a width write that
316
+ > resolves to the current value is a no-op, and on a panel whose width has never
317
+ > moved these two gestures emit nothing. When the width _has_ moved, nothing
318
+ > changes visually at that moment (the rail stays collapsed), but a
319
+ > controlled consumer observes the new width, and it takes effect the moment the
320
+ > panel is expanded by other means (a controlled `expanded` prop). Every _other_
321
+ > expand-triggering gesture — a single click, a drag, the grow Arrow, Enter/Space
322
+ > — is genuinely inert in that state.
323
+
324
+ #### A disabled collapse trigger looks inert
325
+
326
+ **Given** a SidebarSecondary with `collapsible={false}` (or an explicitly
327
+ `disabled` `SidebarSecondaryCollapseTrigger`)
328
+ **When** the footer collapse trigger renders
329
+ **Then** it shows a `not-allowed` cursor instead of the pointer cursor
330
+ **And** its label and icon use the disabled on-surface foreground token
331
+ **And** hovering or pressing it produces no highlight — the hover and active
332
+ fills stay pinned to the row's idle container color
333
+ **And** this is in addition to the native `disabled` attribute above, so the
334
+ row reads as visually inert exactly where it is functionally inert
335
+
336
+ #### Resizing stays live and clamps at the minimum
337
+
338
+ **Given** an expanded, `resizable` SidebarSecondary with `collapsible={false}`
339
+ **When** the user drags the resize edge below the collapse threshold, or presses
340
+ the shrink Arrow so the next step would fall under `minWidth`
341
+ **Then** the width clamps to `minWidth` (the expanded-width token's shipped
342
+ default, 256px) instead of collapsing
343
+ **And** drag/Arrow resizing between `minWidth` and `maxWidth` behaves exactly as
344
+ it does when `collapsible` is `true`
345
+
346
+ #### A permanently collapsed rail is a valid combination
347
+
348
+ **Given** `collapsible={false}` together with `defaultExpanded={false}`
349
+ **When** the panel renders
350
+ **Then** it renders as the collapsed breadcrumb rail (`data-state="collapsed"`)
351
+ **And** no user interaction can expand it — only a controlled `expanded` prop can
352
+ **And** the resize edge stays focusable and keeps its accessible name, but is
353
+ inert: dragging it, clicking it, and the grow/shrink Arrow keys produce no
354
+ observable change
355
+ **And** the one exception is the width reset — `Home` **and** a double-click on
356
+ the edge both still write `defaultWidth` while the rail stays visually
357
+ unchanged, firing `onWidthChange` only when `defaultWidth` differs from the
358
+ current stored width (see the width-reset note above)
359
+
360
+ > `collapsible` never forces `expanded` to a value; it only gates the
361
+ > user-initiated transitions. A controlled `expanded` prop keeps full authority
362
+ > either way.
363
+
364
+ #### The expanded default tooltip drops the click gesture
365
+
366
+ **Given** `collapsible={false}`, an **expanded** panel, and no
367
+ `resizeTooltipExpanded` override
368
+ **When** the user hovers the resize edge
369
+ **Then** the tooltip reads "**Resize:** Drag" and "**Reset size:** Double click"
370
+ — the "**Collapse:** Click" line the `collapsible={true}` default carries is
371
+ omitted, because clicking the edge can no longer collapse the panel
372
+ **And** an explicit `resizeTooltipExpanded` value still wins, including `null`
373
+ to suppress the tooltip
374
+
375
+ #### A permanently collapsed rail's tooltip advertises only the double-click
376
+
377
+ **Given** `collapsible={false}`, a **collapsed** panel, and no
378
+ `resizeTooltipCollapsed` override
379
+ **When** the user hovers the resize edge
380
+ **Then** the tooltip reads "**Reset size:** Double click" — and nothing else
381
+ **And** the other two lines the `collapsible={true}` collapsed default carries
382
+ are dropped, because neither gesture is live: clicking the edge bails in
383
+ `handleClick`, and the collapsed drag branch only ever calls `toggleExpanded()`
384
+ (itself gated off) and never writes a width, so both "**Resize:** Drag" and
385
+ "**Expand:** Click" would be false claims
386
+ **And** the double-click line _is_ honest: `handleDoubleClick` writes
387
+ `defaultWidth` unconditionally, so it stays a live pointer gesture — the pointer
388
+ counterpart of `Home`
389
+ **And** an explicit `resizeTooltipCollapsed` value still wins, including `null`
390
+ to suppress the tooltip entirely
391
+
392
+ #### The collapse trigger shows no tooltip at all
393
+
394
+ **Given** `collapsible={false}` and a footer `SidebarSecondaryCollapseTrigger`
395
+ **When** the user hovers or focuses that trigger while the panel is collapsed
396
+ **Then** no tooltip appears — the trigger's own `Tooltip` is disabled whenever
397
+ the panel is expanded **or** not collapsible
398
+ **And** this holds even when an explicit `expandTooltip` value is passed, unlike
399
+ the two resize-edge tooltips where an override still wins: the trigger is
400
+ natively `disabled` here, so any "Expand" copy would advertise an action the
401
+ user cannot perform
402
+
259
403
  ---
260
404
 
261
405
  ### Disclosure (expandable sections)
@@ -360,6 +504,33 @@ When collapsed, the section list is hidden and a vertical breadcrumb is shown
360
504
  (`aria-hidden`). Any menu-item labels that remain are kept in the DOM as
361
505
  `sr-only`, never `display:none`.
362
506
 
507
+ ### Collapse trigger (footer)
508
+
509
+ `SidebarSecondaryCollapseTrigger` is a `<button>` that toggles the panel width.
510
+
511
+ | `collapsible` | Exposed as | `expandTooltip` while collapsed |
512
+ | ------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------- |
513
+ | `true` | `aria-expanded` mirrors the panel's `expanded` state; button enabled unless the consumer passes `disabled` | shown on hover/focus |
514
+ | `false` | **no** `aria-expanded`; button carries the native `disabled` | **suppressed** (no tooltip) |
515
+
516
+ When `collapsible` is `false` the button has no state to expose or flip, so
517
+ `aria-expanded` is omitted rather than pinned to a value a user cannot change,
518
+ and `disabled` communicates the unavailable action to assistive tech. Its
519
+ `expandTooltip` is suppressed for the same reason — under `collapsible={false}`
520
+ the panel can never be expanded by the user, so an "Expand" hint would be a
521
+ false affordance. (The tooltip is likewise suppressed whenever the panel is
522
+ expanded, in either mode: there is nothing to expand.)
523
+
524
+ The suppression keys off `collapsible`, not off the button's disabled state:
525
+ passing `disabled` directly to the trigger while `collapsible` is `true` still
526
+ disables the button, but the `expandTooltip` keeps showing on hover.
527
+
528
+ ### Resize edge
529
+
530
+ When `resizable`, the panel renders a focusable handle on its inline-end border
531
+ with `role="separator"`, `aria-orientation="vertical"`, `tabIndex={0}` and an
532
+ overridable `aria-label` (default `'Resize sidebar'`).
533
+
363
534
  ---
364
535
 
365
536
  ## Keyboard Navigation
@@ -374,6 +545,55 @@ When collapsed, the section list is hidden and a vertical breadcrumb is shown
374
545
  No roving tabindex — this is a list of links plus disclosure buttons, not a
375
546
  `menu`/`menubar`.
376
547
 
548
+ ### Resize edge (`role="separator"`)
549
+
550
+ Arrow keys follow the writing direction: the **grow** key is `ArrowRight` in LTR
551
+ and `ArrowLeft` in RTL; the **shrink** key is the mirror.
552
+
553
+ | Key | `collapsible: true` | `collapsible: false` |
554
+ | ----------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
555
+ | Grow key | Expanded: +16px. Collapsed: expands the panel | Expanded: +16px. Collapsed: **inert** |
556
+ | Shrink key | Expanded: −16px; below the minimum width it collapses the panel. Collapsed: **inert** | Expanded: −16px, **clamps at the minimum width**, never collapses. Collapsed: **inert** |
557
+ | Enter/Space | Toggles expanded/collapsed | **Inert** |
558
+ | Home | Expands (if collapsed) and resets to the default width | Resets to the default width; never expands |
559
+
560
+ With `collapsible={false}` the edge stays focusable and keeps its accessible
561
+ name. **While the panel is expanded**, resizing is still a real operation — only
562
+ the collapse transition is suppressed. **While the panel is permanently
563
+ collapsed** (`collapsible={false}` + collapsed) the edge is a focusable but
564
+ visually inert tab stop: it keeps its `role="separator"` and accessible name,
565
+ yet dragging, clicking, and both Arrow keys produce no observable change. Every
566
+ **expand-triggering** gesture is inert here: dragging, a single click, the grow
567
+ key and Enter/Space all fire no `toggleExpanded`, so `data-state` stays
568
+ `collapsed`. **Two gestures remain live** — `Home` on the keyboard and a
569
+ **double-click** on the edge. Both reset the stored width to `defaultWidth`, and
570
+ neither is gated on `collapsible` (in `handleDoubleClick`, only the
571
+ `toggleExpanded()` call is). They fire `onWidthChange` **only when
572
+ `defaultWidth` differs from the current stored width** — `setWidth` returns
573
+ early on `next === currentWidth`, so a width write that resolves to the current
574
+ value is a no-op, and on a panel whose width has never moved these two gestures
575
+ write nothing at all. When the width _has_ moved, the callback fires even though
576
+ the rail is collapsed and looks unchanged at that moment; the reset only becomes
577
+ visible if the panel is later expanded by other means (a controlled `expanded`
578
+ prop flip), so a controlled consumer that mirrors `onWidthChange` must expect a
579
+ width update while the panel is collapsed.
580
+
581
+ > **Tooltip copy.** With `collapsible={false}` every tooltip on the panel is
582
+ > adjusted for you, so the copy never advertises a gesture that is inert. The
583
+ > footer collapse trigger's own `expandTooltip` is **suppressed outright** —
584
+ > nothing can expand the panel, and an explicit value does not bring the
585
+ > tooltip back — and both resize-edge tooltip defaults narrow.
586
+ > **Expanded**, the resize-edge default drops the "**Collapse:** Click" line
587
+ > and reads "**Resize:** Drag" / "**Reset size:** Double click". **Collapsed**,
588
+ > the default
589
+ > narrows to the single line "**Reset size:** Double click" — dragging and
590
+ > clicking a permanently collapsed rail do nothing, but a double-click still
591
+ > writes the default width, so that is the one pointer gesture the tooltip can
592
+ > honestly advertise. An explicit `resizeTooltipExpanded` /
593
+ > `resizeTooltipCollapsed` value always wins over the default (including `null`
594
+ > to suppress the tooltip entirely), `collapsible` notwithstanding. All of these
595
+ > strings are English by default and must be localized regardless.
596
+
377
597
  ---
378
598
 
379
599
  ## Focus
@@ -415,3 +635,14 @@ A keyboard-focused row shows a visible focus ring using the shared
415
635
  - [ ] Disclosure trigger exposes `aria-expanded` / `aria-controls`; toggles on click + Enter/Space
416
636
  - [ ] Level-2 sub-items are indented and reachable via Tab when open
417
637
  - [ ] Collapsed rows keep an accessible name (sr-only label, not removed)
638
+ - [ ] Resize edge is `role="separator"`, focusable, and has an overridable `aria-label`
639
+ - [ ] With `collapsible={false}`: the footer collapse trigger is `disabled`,
640
+ exposes no `aria-expanded`, and shows **no Expand tooltip** on hover or
641
+ focus; Enter/Space on the resize edge does not change
642
+ `data-state`; the shrink key clamps at the minimum width
643
+ - [ ] With `collapsible={false}` while collapsed: Home **and a double-click on
644
+ the edge** leave `data-state` `collapsed` but still reset the stored width
645
+ (`onWidthChange` fires with `defaultWidth` only if that differs from the
646
+ current stored width)
647
+ - [ ] With `collapsible={false}` while collapsed: the default resize tooltip
648
+ reads "Reset size: Double click" only
@@ -0,0 +1,359 @@
1
+ # TreeItem
2
+
3
+ > One flat row of a tree or nested-list UI — an optional expand chevron, an optional checkbox, an optional leading icon, the row title, and an optional trailing extras slot. It is a single row and nothing more: it renders no nested list and implements no expand/collapse, so `isExpandable` and `expanded` are purely visual affordances and `selected` is a prop the consumer drives. The tree itself — its ARIA roles, indentation, expand state, selection, and keyboard roving — belongs to the composition, the same way `Breadcrumb` ships the trail's primitives without owning the trail.
4
+
5
+ **Status:** ready · **Category:** navigation · **Since:** v5.1.0
6
+
7
+ ## Import
8
+
9
+ ```tsx
10
+ import { TreeItem } from '@acronis-platform/ui-react'
11
+ ```
12
+
13
+ ## Quick example
14
+
15
+ ```tsx
16
+ <ul role="tree" aria-label="Workloads">
17
+ <li role="treeitem" aria-expanded={open}>
18
+ <TreeItem
19
+ expanded={open}
20
+ hasIcon
21
+ icon={<FolderIcon size={16} />}
22
+ title="All workloads"
23
+ onClick={() => setOpen((v) => !v)}
24
+ onKeyDown={(event) => {
25
+ if (event.key === 'Enter' || event.key === ' ') {
26
+ event.preventDefault();
27
+ setOpen((v) => !v);
28
+ }
29
+ }}
30
+ >
31
+ <Tag variant="info">24</Tag>
32
+ </TreeItem>
33
+ <ul role="group" className="ps-4">
34
+ <li role="treeitem" aria-selected>
35
+ <TreeItem
36
+ isExpandable={false}
37
+ hasIcon
38
+ selected
39
+ title="Cloud applications"
40
+ />
41
+ </li>
42
+ </ul>
43
+ </li>
44
+ </ul>
45
+ ```
46
+
47
+ One flat row of a tree or nested-list UI: an optional expand chevron, an optional
48
+ checkbox, an optional leading icon, the row title, and an optional trailing
49
+ extras slot. It is deliberately **one row** — it renders no nested list, owns no
50
+ expand state, and selects nothing. You compose the rows, indent them, hold the
51
+ open/selected state, and supply the ARIA tree semantics.
52
+
53
+ ## When to use
54
+
55
+ - A file tree, a workload/resource hierarchy, a nested navigation panel — one
56
+ `TreeItem` per row.
57
+ - A multi-select hierarchy: turn on `hasCheckbox` and drive the box through
58
+ `checkboxProps`.
59
+
60
+ ## When not to use
61
+
62
+ - For a flat, non-hierarchical secondary nav — use
63
+ [`SidebarSecondary`](../sidebar-secondary/README.md).
64
+ - For a path back to the current location — use
65
+ [`Breadcrumb`](../breadcrumb/README.md).
66
+ - For collapsible **content** panels rather than a hierarchy of rows — use
67
+ `Accordion` / `AccordionContainer`.
68
+ - As the whole tree. There is no `Tree` container component today; the row is the
69
+ primitive and the tree is assembled in the application.
70
+
71
+ ## Usage
72
+
73
+ ```tsx
74
+ import { TreeItem } from '@acronis-platform/ui-react';
75
+
76
+ const [open, setOpen] = React.useState(true);
77
+
78
+ <ul role="tree" aria-label="Workloads">
79
+ <TreeItem
80
+ render={<li role="treeitem" aria-expanded={open} aria-level={1} />}
81
+ expanded={open}
82
+ hasIcon
83
+ icon={<FolderIcon size={16} />}
84
+ title="All workloads"
85
+ onClick={() => setOpen((v) => !v)}
86
+ >
87
+ <Tag variant="info">24</Tag>
88
+ </TreeItem>
89
+
90
+ {open && (
91
+ <ul role="group" className="ps-4">
92
+ <TreeItem
93
+ render={<li role="treeitem" aria-level={2} aria-selected />}
94
+ isExpandable={false}
95
+ hasIcon
96
+ selected
97
+ title="Cloud applications"
98
+ />
99
+ <TreeItem
100
+ render={<li role="treeitem" aria-level={2} />}
101
+ isExpandable={false}
102
+ hasIcon
103
+ title="Unmanaged workloads"
104
+ />
105
+ </ul>
106
+ )}
107
+ </ul>;
108
+ ```
109
+
110
+ Three things the row hands back to you on purpose:
111
+
112
+ - **`isExpandable` and `expanded` are artwork, not behavior.** The first draws
113
+ the chevron, the second rotates it a quarter turn to point down. Neither
114
+ expands anything, and there is no change event: the Figma node's expandable
115
+ slot has no interaction either. Pass `expanded` from the same boolean that
116
+ feeds your `aria-expanded`, so the affordance and the announced state agree.
117
+ - **`selected` is a look you drive.** The row never selects itself on click; it
118
+ only reflects the flag. Figma's `state=active` swatch paints the same fill,
119
+ which is why `active` is not wired to CSS `:active`.
120
+ - **The ARIA tree is yours.** No `role="treeitem"`, no tab stop by default — see
121
+ [`accessibility.md`](./accessibility.md) for why a role on an orphan row would
122
+ make the tree invalid rather than accessible.
123
+
124
+ ## Parts
125
+
126
+ | Part | Element | Notes |
127
+ | ---------- | ------- | ------------------------------------------------------------------------------------------------- |
128
+ | row (root) | `div` | Polymorphic via `render`; carries the hover / `selected` fill and focus ring |
129
+ | `expander` | `span` | Optional (`isExpandable`, default on); `aria-hidden` chevron, flips in RTL, rotates on `expanded` |
130
+ | `checkbox` | `span` | Optional (`hasCheckbox`); the shared `Checkbox`, `aria-label` defaults to title |
131
+ | `icon` | `span` | Optional (`hasIcon`); falls back to the design's square-dashed placeholder |
132
+ | `title` | `span` | The row label; takes the remaining width and truncates |
133
+ | `extras` | `span` | Optional (`hasExtras`, default on); the trailing `children` slot |
134
+
135
+ ## Design status
136
+
137
+ **No dedicated token tier.** `tokens-pd` ships no `Tree` tier: the Figma node's
138
+ variables still sit in the pre-next-gen `componentLegacy/tree/*` and
139
+ `componentLegacy/sidebar/*` namespaces, which the `brand.components` migration
140
+ has not reached. Every colour the row paints therefore comes from the semantic
141
+ tier, and the layout numbers (8px gap/padding, 128px minimum row width, 4px slot
142
+ inset, 16px minimum extras well) are plain utilities matching the design's pixel
143
+ values — see [`tokens.yaml`](./tokens.yaml) for the full reasoning. Re-point both
144
+ when a `--ui-tree-item-*` tier lands.
145
+
146
+ **Deviation from the Figma frame — the extras well.** Figma measures the trailing
147
+ slot as `min-w-[16px] w-[24px]`; the 24px is the resolved width of its
148
+ placeholder content in a hug-content frame, not a constraint. The implementation
149
+ keeps the 16px minimum and lets the slot grow, because a hard 24px would clip the
150
+ action buttons and count badges the slot exists to hold.
151
+
152
+ ## Props
153
+
154
+ | Prop | Type | Default | Required | Description |
155
+ |------|------|---------|----------|-------------|
156
+ | `title` | `string` | `Title` | no | The row label. Truncates rather than wrapping. Note this shadows the native `title` tooltip attribute on the underlying element: the Figma component names the property `title`, and it is the row's visible label. |
157
+ | `isExpandable` | `boolean` | `true` | no | Show the leading expand chevron. Purely a visual affordance — the row implements no expand/collapse and renders no nested list, and emits no change event. Set it false for a leaf row. |
158
+ | `expanded` | `boolean` | `false` | no | Whether the row's nested list is currently expanded. Purely visual — like `isExpandable`, the row owns no expand/collapse state and renders no nested list. Rotates the leading chevron to reflect the state the consumer is already tracking (and should also be reflecting via `aria-expanded` on its own element, e.g. `<li role="treeitem" aria-expanded={open}>`). Has no effect when `isExpandable` is false. |
159
+ | `hasCheckbox` | `boolean` | `false` | no | Show a leading checkbox, for a multi-select tree. |
160
+ | `checkboxProps` | `CheckboxProps` | — | no | Props forwarded verbatim to the leading checkbox (`checked`, `onCheckedChange`, `indeterminate`, `aria-label`, …). The row keeps no checked state of its own, so the consumer controls the box exactly as it would a standalone `Checkbox`. `aria-label` defaults to `title`. |
161
+ | `hasIcon` | `boolean` | `false` | no | Show the leading icon slot. |
162
+ | `icon` | `ReactNode` | — | no | The leading icon. Only rendered when `hasIcon` is set; falls back to the design's square-dashed placeholder icon, which is what the Figma node draws. |
163
+ | `hasExtras` | `boolean` | `true` | no | Render the trailing extras slot at all. When false, `children` are dropped entirely rather than merely hidden. |
164
+ | `selected` | `boolean` | `false` | no | Whether the row is selected — the Figma `variant` axis. Paints the persistent highlighted background, the same fill the design's `state=active` swatch previews. Selection is the consumer's state to own; the row only reflects it. |
165
+ | `render` | `RenderProp` | — | no | Base UI composition prop — replaces the rendered `<div>` while merging props and classes. This is how a real ARIA tree supplies the semantics the standalone row deliberately does not force (`<li role="treeitem" aria-expanded aria-level>`). React-only; other frameworks use their own polymorphism. |
166
+
167
+ ## Events
168
+
169
+ | Event | Payload | Description |
170
+ |-------|---------|-------------|
171
+ | `onClick` | `(event: MouseEvent) => void` | Fired when the row is clicked. Expanding and selecting are the consumer's to implement — the row emits nothing of its own for either. |
172
+
173
+ ## Content slots
174
+
175
+ | Slot | Description |
176
+ |------|-------------|
177
+ | `default` | The trailing extras slot: action buttons, a count badge. Rendered only when `hasExtras` is true. |
178
+
179
+ ## Behavior
180
+
181
+ ### Renders one row
182
+
183
+ - **Given** no props
184
+ **Then** the row renders the expand chevron (`isExpandable` defaults to true),
185
+ the default `title`, and an empty extras slot — exactly the Figma default.
186
+ - **Given** a `title`
187
+ **Then** it renders in the row's remaining width and truncates rather than
188
+ wrapping or widening the row.
189
+ - **Given** `hasIcon`
190
+ **Then** the leading icon slot renders, using `icon` if supplied and the
191
+ design's square-dashed placeholder otherwise.
192
+ - **Given** an `icon` **but not** `hasIcon`
193
+ **Then** nothing renders — `hasIcon` is the gate, matching the Figma boolean.
194
+ - **Given** `hasCheckbox`
195
+ **Then** the shared `Checkbox` renders before the icon, unlabelled, with its
196
+ `aria-label` defaulting to `title`.
197
+ - **Given** `checkboxProps`
198
+ **Then** they are forwarded verbatim to that checkbox, including an
199
+ `aria-label` that overrides the `title` default. The row itself keeps no
200
+ checked state.
201
+ - **Given** `isExpandable={false}`
202
+ **Then** the chevron is not rendered and the row reads as a leaf.
203
+ - **Then** the slot order is always chevron, checkbox, icon, title, extras.
204
+
205
+ ### The extras slot is gated, not hidden
206
+
207
+ - **Given** `children` **and** `hasExtras` (the default)
208
+ **Then** the trailing slot renders them, reserving a 16px minimum so an empty
209
+ slot still keeps a column of rows aligned.
210
+ - **Given** `children` **and** `hasExtras={false}`
211
+ **Then** the slot and the children are removed from the DOM entirely — a
212
+ button in there is not merely invisible, it does not exist, so it cannot be
213
+ tabbed to.
214
+
215
+ ### Selection is a prop, not an interaction
216
+
217
+ - **Given** `selected={false}` (the default)
218
+ **Then** the row background is transparent.
219
+ - **Given** the pointer over the row
220
+ **Then** the background takes the hover token — regardless of `selected`,
221
+ since the design draws hover as its own swatch.
222
+ - **Given** `selected`
223
+ **Then** the background takes the highlighted `--ui-background-surface-active`
224
+ fill persistently, and the row carries `data-selected` so a consumer can key
225
+ off it. This is the same fill the Figma `state=active` swatch previews.
226
+ - **Given** a click
227
+ **Then** `onClick` fires and nothing else changes: the row does not select
228
+ itself. Selection belongs to the tree the consumer composes.
229
+
230
+ ### Expansion is reflected, not implemented
231
+
232
+ - **Given** a click on the chevron
233
+ **Then** nothing expands. The chevron is `aria-hidden` artwork; the row renders
234
+ no nested list and emits no expand event, matching the Figma node, whose
235
+ expandable slot has no interaction either.
236
+ - The consumer owns the expand state, renders the child level itself, and puts
237
+ `aria-expanded` on the element it supplies through `render`.
238
+ - **Given** `expanded={false}` (the default)
239
+ **Then** the chevron points along the inline-end direction — the collapsed
240
+ affordance.
241
+ - **Given** `expanded`
242
+ **Then** the chevron rotates a quarter turn to point down, so the affordance
243
+ agrees with the `aria-expanded` the consumer publishes. It is a reflection of
244
+ the consumer's state, not a state the row holds: nothing else about the row
245
+ changes, no nested list appears, and no event fires.
246
+ - **Given** `expanded` **and** `isExpandable={false}`
247
+ **Then** it has no effect — there is no chevron to rotate.
248
+
249
+ ### Focus
250
+
251
+ - **Given** the default rendering
252
+ **Then** the row is not in the tab order — it sets no `tabIndex` and no role.
253
+ - **Given** a consumer-supplied `tabIndex` (or a composed focusable element)
254
+ **Then** keyboard focus paints the library's standard 3px `--ui-focus-primary`
255
+ ring, flush to the row's 2px radius. Mouse focus does not, because it is a
256
+ `:focus-visible` ring.
257
+
258
+ ### RTL
259
+
260
+ - **Given** `dir="rtl"`
261
+ **Then** the whole row mirrors: the slot order, the gap, and the padding are
262
+ all logical, and the collapsed chevron artwork itself flips via an explicit
263
+ `rtl:rotate-180` — logical layout alone would leave it pointing the wrong way.
264
+ - **Given** `dir="rtl"` **and** `expanded`
265
+ **Then** the chevron points down, the same as in LTR: "open" is a
266
+ direction-agnostic direction, so the expanded rotation replaces the RTL mirror
267
+ rather than compounding with it.
268
+
269
+ ### Composition
270
+
271
+ - **Given** a `render` prop
272
+ **Then** the rendered element is replaced (e.g. by `<li role="treeitem">`) and
273
+ the row's props, classes, and data attributes merge onto it.
274
+ - **Given** native `<div>` attributes (`onClick`, `tabIndex`, `aria-*`,
275
+ `className`)
276
+ **Then** they pass through, and `className` merges with the row's own classes.
277
+
278
+ ### Not owned here
279
+
280
+ - The tree: its `role="tree"` owner, indentation, `aria-level`, keyboard roving,
281
+ and type-ahead. The consumer composes rows and supplies all of it.
282
+ - Expand/collapse state and the nested list. The row only mirrors that state
283
+ visually, through `expanded`.
284
+ - Selection state, single vs. multi-select, and the checkbox's checked value.
285
+
286
+ ## Accessibility
287
+
288
+ - **No `role="treeitem"` is forced, on purpose.** A tree item is only valid
289
+ inside a `role="tree"` (or `role="group"`) owner, and to be announced usefully
290
+ it needs `aria-expanded`, `aria-level`, `aria-selected`, and a roving
291
+ `tabindex` — none of which a standalone row can know. Stamping the role on an
292
+ orphan row would produce an **invalid** tree, not an accessible one. The row is
293
+ a plain `<div>`, and `render` is how the consumer supplies the real semantics:
294
+
295
+ ```tsx
296
+ <ul role="tree" aria-label="Workloads">
297
+ <TreeItem render={<li role="treeitem" aria-expanded={open} aria-level={1} />} … />
298
+ </ul>
299
+ ```
300
+
301
+ This is the same boundary `Breadcrumb` draws — it ships `nav`/`ol`/`li` and the
302
+ current-page part, not the trail — and `StepperItem`, which documents that
303
+ sequence semantics belong to the container.
304
+
305
+ - **Not in the tab order by default.** The row sets no `tabIndex`, so it never
306
+ becomes a silent tab stop inside a tree that manages focus itself. Pass
307
+ `tabIndex` (or compose a focusable element) when your tree wants the row
308
+ focusable; a roving `tabindex` — exactly one focusable row — is the pattern the
309
+ ARIA tree view calls for.
310
+
311
+ - **Keyboard focus is visible.** The row carries the library's standard 3px
312
+ `--ui-focus-primary` `focus-visible` ring, the same one `Button` and
313
+ `BreadcrumbLink` use, so a focusable row is never focused invisibly.
314
+
315
+ - **The chevron is `aria-hidden` decoration.** It is a visual affordance only:
316
+ the row implements no expand/collapse, so there is no state for it to announce.
317
+ Announce expansion with `aria-expanded` on the element you compose — that is
318
+ where the state actually lives. Do **not** give the chevron its own button
319
+ role; a row that only _looks_ expandable would then promise an action nothing
320
+ handles.
321
+
322
+ - **Keep `expanded` and `aria-expanded` in step.** `expanded` rotates the chevron
323
+ but publishes nothing to assistive technology; `aria-expanded` on your `render`
324
+ element publishes the state but paints nothing. They are two renderings of the
325
+ one boolean you own, so drive both from it (`aria-expanded={open}` +
326
+ `expanded={open}`). Setting only one leaves the sighted affordance and the
327
+ announced state disagreeing — which is the bug `expanded` exists to close.
328
+
329
+ - **The checkbox is always named.** Composed without a visible label, it would
330
+ otherwise be an unnamed control, so its `aria-label` defaults to `title`.
331
+ Override it through `checkboxProps` when the row title is not the right name
332
+ for the box (e.g. `aria-label="Select all workloads"`).
333
+
334
+ - **Selection is announced by the consumer.** `selected` is a _visual_ fill.
335
+ Colour alone is not a status, so put `aria-selected` on the element you compose
336
+ via `render` (and `aria-current` if the row marks a location rather than a
337
+ selection). The row also exposes `data-selected` for styling and testing.
338
+
339
+ - **One localizable string.** `title` is the only text the component renders on
340
+ its own, and the literal `'Title'` appears only as that prop's default —
341
+ callers pass a localized value. Everything else (`children`, `icon`,
342
+ `checkboxProps['aria-label']`) is consumer-supplied.
343
+
344
+ - **RTL**: the slot order, gap, and padding are logical, so the row mirrors under
345
+ `dir="rtl"` on its own. The collapsed chevron is direction-sensitive
346
+ **artwork**, which logical properties cannot mirror, so it carries an explicit
347
+ `rtl:rotate-180`. The `expanded` chevron points down in both directions —
348
+ "open" has no inline direction to mirror.
349
+
350
+ ## Contrast
351
+
352
+ The title uses `--ui-text-on-surface-primary` and the glyphs
353
+ `--ui-glyph-on-surface-primary`, both over the row's transparent, hover, or
354
+ `selected` background (`--ui-background-surface-hover` /
355
+ `--ui-background-surface-active`). All are semantic-tier tokens defined per brand
356
+ and light/dark mode via `light-dark()`. Re-check the title and glyph contrast
357
+ against the **selected** fill specifically when a brand overrides either — the
358
+ idle case sits on the page surface, the selected case does not. Should a
359
+ dedicated `--ui-tree-item-*` tier land (see `tokens.yaml`), re-check all pairs.