prime-ui-kit 0.13.1 → 0.14.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 (215) hide show
  1. package/README.md +7 -6
  2. package/dist/chunks/{chunk-XNOJEX55.js → chunk-P6ZO37JM.js} +1 -1
  3. package/dist/chunks/{chunk-JV2KB5JZ.js → chunk-SYEX7OVT.js} +3572 -2865
  4. package/dist/chunks/{chunk-PK2GC77E.js → chunk-WXKRUVSE.js} +164 -140
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/banner/Banner.d.ts +5 -2
  7. package/dist/components/chart-frame/ChartFrame.d.ts +19 -6
  8. package/dist/components/chat/Chat.d.ts +2 -2
  9. package/dist/components/chat/ChatPinned.d.ts +2 -2
  10. package/dist/components/chat/MessageList.d.ts +1 -1
  11. package/dist/components/chat/chatContext.d.ts +1 -1
  12. package/dist/components/field-list/FieldList.d.ts +90 -0
  13. package/dist/components/index.d.ts +3 -1
  14. package/dist/components/input/Input.d.ts +2 -2
  15. package/dist/components/kanban/Kanban.d.ts +6 -3
  16. package/dist/components/page-content/PageContent.d.ts +55 -4
  17. package/dist/components/sparkline/Sparkline.d.ts +64 -15
  18. package/dist/components/sparkline/geometry.d.ts +26 -0
  19. package/dist/components/tooltip/Tooltip.d.ts +5 -2
  20. package/dist/hooks/usePosition.d.ts +15 -4
  21. package/dist/index.css +1 -1
  22. package/dist/index.js +4 -2
  23. package/dist/internal/ChartTooltip.d.ts +26 -0
  24. package/dist/internal/FieldAction.d.ts +6 -7
  25. package/dist/internal/fieldCell.d.ts +21 -0
  26. package/dist/internal/messageList.d.ts +2 -2
  27. package/dist/internal/palette.d.ts +7 -0
  28. package/dist/skill/SKILL.md +24 -14
  29. package/dist/skill/anti-slop.md +77 -4
  30. package/dist/skill/cheatsheet.md +24 -5
  31. package/dist/skill/checklist.md +19 -1
  32. package/dist/skill/choosing.md +10 -5
  33. package/dist/skill/components.md +4 -3
  34. package/dist/skill/composition.md +28 -15
  35. package/dist/skill/foundations.md +8 -6
  36. package/dist/skill/layouts.md +6 -6
  37. package/dist/skill/patterns/app-shell.module.css +1 -8
  38. package/dist/skill/patterns/app-shell.tsx +11 -9
  39. package/dist/skill/patterns/chat.tsx +23 -5
  40. package/dist/skill/patterns/dashboard.module.css +1 -31
  41. package/dist/skill/patterns/dashboard.tsx +150 -136
  42. package/dist/skill/patterns/detail-page.module.css +0 -3
  43. package/dist/skill/patterns/detail-page.tsx +4 -7
  44. package/dist/skill/patterns/form-drawer.tsx +2 -2
  45. package/dist/skill/patterns/kanban-board.tsx +4 -2
  46. package/dist/skill/patterns/list-page.tsx +2 -2
  47. package/dist/skill/patterns/media-library.module.css +0 -22
  48. package/dist/skill/patterns/media-library.tsx +17 -18
  49. package/dist/skill/patterns/record-editor.module.css +18 -0
  50. package/dist/skill/patterns/record-editor.tsx +356 -0
  51. package/dist/skill/patterns/screen-states.module.css +3 -17
  52. package/dist/skill/patterns/screen-states.tsx +70 -69
  53. package/dist/skill/patterns/settings-page.module.css +1 -21
  54. package/dist/skill/patterns/settings-page.tsx +58 -79
  55. package/dist/skill/reference/components/avatar/COMPONENT.md +4 -4
  56. package/dist/skill/reference/components/avatar/examples/presence.tsx +1 -1
  57. package/dist/skill/reference/components/avatar/examples/states.tsx +1 -1
  58. package/dist/skill/reference/components/banner/COMPONENT.md +6 -6
  59. package/dist/skill/reference/components/banner/examples/dismissible.tsx +22 -11
  60. package/dist/skill/reference/components/banner/examples/examples.module.css +3 -3
  61. package/dist/skill/reference/components/banner/examples/narrow.tsx +1 -1
  62. package/dist/skill/reference/components/breadcrumb/COMPONENT.md +1 -1
  63. package/dist/skill/reference/components/breadcrumb/examples/overflow.tsx +1 -1
  64. package/dist/skill/reference/components/card/COMPONENT.md +5 -4
  65. package/dist/skill/reference/components/card/examples/examples.module.css +0 -28
  66. package/dist/skill/reference/components/card/examples/kpi-media.tsx +13 -11
  67. package/dist/skill/reference/components/card/examples/narrow.tsx +1 -1
  68. package/dist/skill/reference/components/card/examples/panel-chart.tsx +45 -22
  69. package/dist/skill/reference/components/chart-frame/COMPONENT.md +10 -7
  70. package/dist/skill/reference/components/chart-frame/examples/tooltip.tsx +20 -24
  71. package/dist/skill/reference/components/chat/COMPONENT.md +3 -3
  72. package/dist/skill/reference/components/chat/examples/search.tsx +1 -1
  73. package/dist/skill/reference/components/chat-list/COMPONENT.md +1 -1
  74. package/dist/skill/reference/components/checkbox/examples/indicator.tsx +24 -2
  75. package/dist/skill/reference/components/color-picker/COMPONENT.md +2 -2
  76. package/dist/skill/reference/components/color-swatches/COMPONENT.md +1 -0
  77. package/dist/skill/reference/components/datepicker/COMPONENT.md +1 -0
  78. package/dist/skill/reference/components/dnd/COMPONENT.md +3 -3
  79. package/dist/skill/reference/components/field-list/COMPONENT.md +164 -0
  80. package/dist/skill/reference/components/field-list/examples/base-row.tsx +76 -0
  81. package/dist/skill/reference/components/field-list/examples/controlled.tsx +72 -0
  82. package/dist/skill/reference/components/field-list/examples/errors.tsx +85 -0
  83. package/dist/skill/reference/components/field-list/examples/examples.module.css +34 -0
  84. package/dist/skill/reference/components/field-list/examples/limits.tsx +55 -0
  85. package/dist/skill/reference/components/field-list/examples/locked.tsx +139 -0
  86. package/dist/skill/reference/components/field-list/examples/narrow.tsx +68 -0
  87. package/dist/skill/reference/components/field-list/examples/overview.tsx +66 -0
  88. package/dist/skill/reference/components/field-list/examples/reorder.tsx +42 -0
  89. package/dist/skill/reference/components/field-list/examples/sizes.tsx +42 -0
  90. package/dist/skill/reference/components/field-list/examples/states.tsx +51 -0
  91. package/dist/skill/reference/components/input/examples/examples.module.css +2 -7
  92. package/dist/skill/reference/components/kanban/COMPONENT.md +13 -9
  93. package/dist/skill/reference/components/kanban/examples/limits.tsx +2 -1
  94. package/dist/skill/reference/components/kanban/examples/variants.tsx +16 -8
  95. package/dist/skill/reference/components/list/COMPONENT.md +1 -1
  96. package/dist/skill/reference/components/media-viewer/COMPONENT.md +2 -2
  97. package/dist/skill/reference/components/message/COMPONENT.md +2 -2
  98. package/dist/skill/reference/components/notification/COMPONENT.md +1 -0
  99. package/dist/skill/reference/components/page-content/COMPONENT.md +70 -14
  100. package/dist/skill/reference/components/page-content/examples/grid.tsx +107 -0
  101. package/dist/skill/reference/components/page-content/examples/overview.tsx +2 -2
  102. package/dist/skill/reference/components/page-content/examples/sections.tsx +81 -0
  103. package/dist/skill/reference/components/page-toolbar/COMPONENT.md +1 -1
  104. package/dist/skill/reference/components/segmented-control/COMPONENT.md +1 -0
  105. package/dist/skill/reference/components/select/COMPONENT.md +4 -2
  106. package/dist/skill/reference/components/select/examples/icon-only.tsx +38 -0
  107. package/dist/skill/reference/components/sparkline/COMPONENT.md +83 -30
  108. package/dist/skill/reference/components/sparkline/examples/compact.tsx +1 -1
  109. package/dist/skill/reference/components/sparkline/examples/curve.tsx +28 -0
  110. package/dist/skill/reference/components/sparkline/examples/gaps.tsx +33 -0
  111. package/dist/skill/reference/components/sparkline/examples/overview.tsx +11 -2
  112. package/dist/skill/reference/components/sparkline/examples/period.tsx +7 -2
  113. package/dist/skill/reference/components/sparkline/examples/reference.tsx +37 -0
  114. package/dist/skill/reference/components/sparkline/examples/variants.tsx +2 -2
  115. package/dist/skill/reference/components/tabs/examples/examples.module.css +0 -6
  116. package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +11 -13
  117. package/dist/skill/reference/components/timeline/examples/examples.module.css +0 -8
  118. package/dist/skill/reference/components/timeline/examples/narrow.tsx +2 -2
  119. package/dist/skill/reference/components/timeline/examples/sizes.tsx +3 -3
  120. package/dist/skill/reference/components/timeline/examples/variants.tsx +4 -4
  121. package/dist/skill/reference/components/tooltip/COMPONENT.md +6 -4
  122. package/dist/skill/reference/components/tooltip/examples/placement.tsx +2 -1
  123. package/dist/skill/reference/components/video-note/COMPONENT.md +1 -1
  124. package/dist/skill/reference/components/voice-player/COMPONENT.md +1 -1
  125. package/dist/skill/reference/layout/app-header/examples/in-app-shell.tsx +2 -2
  126. package/dist/skill/reference/layout/app-shell/COMPONENT.md +1 -1
  127. package/dist/skill/reference/layout/app-shell/examples/footer.tsx +2 -2
  128. package/dist/skill/reference/layout/app-shell/examples/overview.tsx +2 -2
  129. package/dist/skill/reference/layout/app-shell/examples/template.tsx +2 -2
  130. package/dist/skill/reference/layout/bottom-nav/examples/floating-action.tsx +2 -2
  131. package/dist/skill/reference/layout/bottom-nav/examples/floating.tsx +2 -2
  132. package/dist/skill/reference/layout/bottom-nav/examples/in-app-shell.tsx +2 -2
  133. package/dist/skill/responsive.md +5 -3
  134. package/dist/skill/ux.md +145 -0
  135. package/dist/tokens/semantic.d.ts +2 -0
  136. package/package.json +1 -1
  137. package/src/components/avatar/COMPONENT.md +4 -4
  138. package/src/components/avatar/examples/presence.tsx +1 -1
  139. package/src/components/avatar/examples/states.tsx +1 -1
  140. package/src/components/banner/COMPONENT.md +6 -6
  141. package/src/components/banner/examples/dismissible.tsx +22 -11
  142. package/src/components/banner/examples/examples.module.css +3 -3
  143. package/src/components/banner/examples/narrow.tsx +1 -1
  144. package/src/components/breadcrumb/COMPONENT.md +1 -1
  145. package/src/components/breadcrumb/examples/overflow.tsx +1 -1
  146. package/src/components/card/COMPONENT.md +5 -4
  147. package/src/components/card/examples/examples.module.css +0 -28
  148. package/src/components/card/examples/kpi-media.tsx +13 -11
  149. package/src/components/card/examples/narrow.tsx +1 -1
  150. package/src/components/card/examples/panel-chart.tsx +45 -22
  151. package/src/components/chart-frame/COMPONENT.md +10 -7
  152. package/src/components/chart-frame/examples/tooltip.tsx +20 -24
  153. package/src/components/chat/COMPONENT.md +3 -3
  154. package/src/components/chat/examples/search.tsx +1 -1
  155. package/src/components/chat-list/COMPONENT.md +1 -1
  156. package/src/components/checkbox/examples/indicator.tsx +24 -2
  157. package/src/components/color-picker/COMPONENT.md +2 -2
  158. package/src/components/color-swatches/COMPONENT.md +1 -0
  159. package/src/components/datepicker/COMPONENT.md +1 -0
  160. package/src/components/dnd/COMPONENT.md +3 -3
  161. package/src/components/field-list/COMPONENT.md +164 -0
  162. package/src/components/field-list/examples/base-row.tsx +76 -0
  163. package/src/components/field-list/examples/controlled.tsx +72 -0
  164. package/src/components/field-list/examples/errors.tsx +85 -0
  165. package/src/components/field-list/examples/examples.module.css +34 -0
  166. package/src/components/field-list/examples/limits.tsx +55 -0
  167. package/src/components/field-list/examples/locked.tsx +139 -0
  168. package/src/components/field-list/examples/narrow.tsx +68 -0
  169. package/src/components/field-list/examples/overview.tsx +66 -0
  170. package/src/components/field-list/examples/reorder.tsx +42 -0
  171. package/src/components/field-list/examples/sizes.tsx +42 -0
  172. package/src/components/field-list/examples/states.tsx +51 -0
  173. package/src/components/input/examples/examples.module.css +2 -7
  174. package/src/components/kanban/COMPONENT.md +13 -9
  175. package/src/components/kanban/examples/limits.tsx +2 -1
  176. package/src/components/kanban/examples/variants.tsx +16 -8
  177. package/src/components/list/COMPONENT.md +1 -1
  178. package/src/components/media-viewer/COMPONENT.md +2 -2
  179. package/src/components/message/COMPONENT.md +2 -2
  180. package/src/components/notification/COMPONENT.md +1 -0
  181. package/src/components/page-content/COMPONENT.md +70 -14
  182. package/src/components/page-content/examples/grid.tsx +107 -0
  183. package/src/components/page-content/examples/overview.tsx +2 -2
  184. package/src/components/page-content/examples/sections.tsx +81 -0
  185. package/src/components/page-toolbar/COMPONENT.md +1 -1
  186. package/src/components/segmented-control/COMPONENT.md +1 -0
  187. package/src/components/select/COMPONENT.md +4 -2
  188. package/src/components/select/examples/icon-only.tsx +38 -0
  189. package/src/components/sparkline/COMPONENT.md +83 -30
  190. package/src/components/sparkline/examples/compact.tsx +1 -1
  191. package/src/components/sparkline/examples/curve.tsx +28 -0
  192. package/src/components/sparkline/examples/gaps.tsx +33 -0
  193. package/src/components/sparkline/examples/overview.tsx +11 -2
  194. package/src/components/sparkline/examples/period.tsx +7 -2
  195. package/src/components/sparkline/examples/reference.tsx +37 -0
  196. package/src/components/sparkline/examples/variants.tsx +2 -2
  197. package/src/components/tabs/examples/examples.module.css +0 -6
  198. package/src/components/tabs/examples/in-drawer.tsx +11 -13
  199. package/src/components/timeline/examples/examples.module.css +0 -8
  200. package/src/components/timeline/examples/narrow.tsx +2 -2
  201. package/src/components/timeline/examples/sizes.tsx +3 -3
  202. package/src/components/timeline/examples/variants.tsx +4 -4
  203. package/src/components/tooltip/COMPONENT.md +6 -4
  204. package/src/components/tooltip/examples/placement.tsx +2 -1
  205. package/src/components/video-note/COMPONENT.md +1 -1
  206. package/src/components/voice-player/COMPONENT.md +1 -1
  207. package/src/layout/app-header/examples/in-app-shell.tsx +2 -2
  208. package/src/layout/app-shell/COMPONENT.md +1 -1
  209. package/src/layout/app-shell/examples/footer.tsx +2 -2
  210. package/src/layout/app-shell/examples/overview.tsx +2 -2
  211. package/src/layout/app-shell/examples/template.tsx +2 -2
  212. package/src/layout/bottom-nav/examples/floating-action.tsx +2 -2
  213. package/src/layout/bottom-nav/examples/floating.tsx +2 -2
  214. package/src/layout/bottom-nav/examples/in-app-shell.tsx +2 -2
  215. package/src/styles/theme-light.css +1 -0
@@ -163,7 +163,7 @@ Card.Root asChild <a> or <button> (the only child) holding the pa
163
163
  Without `asChild` a card is static: no hover, focus or press of its own. With it: `data-interactive` on the element.
164
164
 
165
165
  ## Layout & spacing
166
- - Grid of cards: `repeat(auto-fit, minmax(min(100%, 14rem), 1fr))`, gap `--prime-space-4`.
166
+ - Cards on a page: one `PageContent.Grid` with a `PageContent.Cell span` per card (16 on both axes, the cards of a row share one height); a grid inside one block is the block's own `repeat(auto-fit, minmax(min(100%, 14rem), 1fr))`, gap `--prime-space-4`.
167
167
  - Radius `--prime-card-radius`, no border. In `panel` and `list` the `Header` is a padded zone with a faint inset hairline below; in `stat-trend` it can pair a leading Badge or icon with a compact `Card.Value`.
168
168
  - Padding `--prime-card-padding-m` (20); `Body` gap `--prime-card-gap` (16); fields inside `Body` 20 apart, one ladder step off the card.
169
169
  - Depth comes from nesting (foundation §4): on the page — the AppShell panel is the page — a card is layer 1 (white in light) with the raised whisper; inside another card it is the next layer, a flat tile. The ladder stops at layer 4.
@@ -196,11 +196,11 @@ No `labels`.
196
196
  | [overview.tsx](examples/overview.tsx) | A company settings panel: a header, fields on the surface fill and buttons in the footer — `Card.Header`, `Card.Body`, `Card.Footer`. |
197
197
  | [variants.tsx](examples/variants.tsx) | KPI templates: an icon tile with a value, a badge with a value in the header row, and a large value with its change and a reference line — `variant`, `Card.Delta`, `Card.Meta`. |
198
198
  | [kpi-media.tsx](examples/kpi-media.tsx) | A KPI with a sparkline or a fill level in the bottom slot of `mini` — `Card.Media`. |
199
- | [panel-chart.tsx](examples/panel-chart.tsx) | A chart widget: a header with a short description and a period switch, a summary line and an edge-to-edge chart — `Card.Header`, `Card.Description`, `Card.Media`. |
199
+ | [panel-chart.tsx](examples/panel-chart.tsx) | A chart widget: a header with a short description and a period switch, and a Sparkline with the period total over its chart — `Card.Header`, `Card.Description`, `Card.Body`. |
200
200
  | [interactive.tsx](examples/interactive.tsx) | Whole cards as links to their reports: one tab stop each, a lift on hover, a press and the focus ring — `asChild`. |
201
201
  | [content-templates.tsx](examples/content-templates.tsx) | Content templates: a call to action, an events list and a campaign tile with a cover — `Card.Footer`, `Card.List`, `Card.Media`. |
202
202
  | [flat.tsx](examples/flat.tsx) | A flat tile without the raised shadow next to the default one, for dense grids — `flat`. |
203
- | [narrow.tsx](examples/narrow.tsx) | The card is a size container: the split template stacks its cells below 22rem and the trend value shrinks below 20rem. |
203
+ | [narrow.tsx](examples/narrow.tsx) | The card is a size container: the split template stacks its cells below 22rem and the trend value shrinks below 20rem — `variant`. |
204
204
 
205
205
  ## Mistakes
206
206
  - A `<div>` with a border styled as a card → use `Card.Root`; depth comes from fill, not lines.
@@ -209,10 +209,11 @@ No `labels`.
209
209
  - Custom padding on a `panel` root → padding belongs to `Header` / `Body` / `Footer`.
210
210
  - A wrapper `<div>` around the label and the value of `mini` → put `Icon`, `Label`, `Value` straight into the root (or into a `split` cell); the template lays them out.
211
211
  - A chart SVG with only a `viewBox` → give it a CSS height.
212
+ - A hand-drawn trend SVG in a chart widget → `Sparkline` in the `Body` (or `Media`); a chart with a legend and states → `ChartFrame`.
212
213
  - A clickable `<div>` card with `onClick` → `asChild` with a real `<a href>` (Cmd-click, a new tab) or a `<button>`.
213
214
  - Buttons or links inside an `asChild` card → nested interactive elements; move them out of the card.
214
215
  - The reference figures written into `Card.Delta` or `Card.Label` → `Card.Meta`.
215
216
 
216
217
  ## Related
217
218
  - **Built from:** —
218
- - **See also:** [PageContent](../page-content/COMPONENT.md), [DescriptionList](../description-list/COMPONENT.md) (record details inside a `Card.Body`), [Fieldset](../fieldset/COMPONENT.md) (groups of fields inside a settings card), [DataTable](../data-table/COMPONENT.md), [Badge](../badge/COMPONENT.md), [ProgressBar](../progress-bar/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md)
219
+ - **See also:** [PageContent](../page-content/COMPONENT.md), [DescriptionList](../description-list/COMPONENT.md) (record details inside a `Card.Body`), [Fieldset](../fieldset/COMPONENT.md) (groups of fields inside a settings card), [DataTable](../data-table/COMPONENT.md), [Badge](../badge/COMPONENT.md), [ProgressBar](../progress-bar/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md), [Sparkline](../sparkline/COMPONENT.md) (a trend with its total inside a `panel`), [ChartFrame](../chart-frame/COMPONENT.md) (a full chart with legend and states)
@@ -28,34 +28,6 @@
28
28
  width: min(100%, calc(var(--prime-space-24) * 6));
29
29
  }
30
30
 
31
- /* Sparkline: fixed height from tokens, stretches to the card width. */
32
- .spark {
33
- display: block;
34
- width: 100%;
35
- height: var(--prime-space-10);
36
- color: var(--prime-color-accent-default);
37
- overflow: visible;
38
- }
39
-
40
- .chart {
41
- display: block;
42
- width: 100%;
43
- /* Fixed height: a viewBox-only SVG would otherwise scale to a square of the card width. */
44
- height: calc(var(--prime-space-24) * 2);
45
- color: var(--prime-color-accent-default);
46
- }
47
-
48
- .chartFill {
49
- fill: var(--prime-color-accent-soft);
50
- }
51
-
52
- .chartLine {
53
- fill: none;
54
- stroke: currentColor;
55
- stroke-width: calc(2 * var(--prime-border-width));
56
- vector-effect: non-scaling-stroke;
57
- }
58
-
59
31
  .cover {
60
32
  background: linear-gradient(
61
33
  135deg,
@@ -1,9 +1,19 @@
1
1
  /** A KPI with a sparkline or a fill level in the bottom slot of `mini` — `Card.Media`. */
2
- import { Card, Icon, ProgressBar } from "prime-ui-kit";
2
+ import { Card, Icon, ProgressBar, Sparkline, type SparklinePoint } from "prime-ui-kit";
3
3
 
4
4
  import styles from "./examples.module.css";
5
5
 
6
- const SPARKLINE = "M0 32 L15 28 L30 30 L45 18 L60 22 L75 12 L90 16 L105 8 L120 4";
6
+ const REQUESTS: SparklinePoint[] = [
7
+ { label: "12:00", value: 9_120 },
8
+ { label: "12:10", value: 9_640 },
9
+ { label: "12:20", value: 9_380 },
10
+ { label: "12:30", value: 10_910 },
11
+ { label: "12:40", value: 10_470 },
12
+ { label: "12:50", value: 11_760 },
13
+ { label: "13:00", value: 11_290 },
14
+ { label: "13:10", value: 12_310 },
15
+ { label: "13:20", value: 12_840 },
16
+ ];
7
17
 
8
18
  export default function CardKpiMediaExample() {
9
19
  return (
@@ -15,15 +25,7 @@ export default function CardKpiMediaExample() {
15
25
  <Card.Label>Запросы в минуту</Card.Label>
16
26
  <Card.Value>12 840</Card.Value>
17
27
  <Card.Media>
18
- {/* The SVG stretches to the card width; CSS sets its height, not the viewBox. */}
19
- <svg
20
- className={styles.spark}
21
- viewBox="0 0 120 40"
22
- preserveAspectRatio="none"
23
- aria-hidden="true"
24
- >
25
- <path d={SPARKLINE} className={styles.chartLine} />
26
- </svg>
28
+ <Sparkline layout="compact" data={REQUESTS} label="Запросы в минуту за час" />
27
29
  </Card.Media>
28
30
  </Card.Root>
29
31
 
@@ -1,4 +1,4 @@
1
- /** The card is a size container: the split template stacks its cells below 22rem and the trend value shrinks below 20rem. */
1
+ /** The card is a size container: the split template stacks its cells below 22rem and the trend value shrinks below 20rem — `variant`. */
2
2
  import { Card, Icon } from "prime-ui-kit";
3
3
 
4
4
  import styles from "./examples.module.css";
@@ -1,43 +1,66 @@
1
- /** A chart widget: a header with a short description and a period switch, a summary line and an edge-to-edge chart — `Card.Header`, `Card.Description`, `Card.Media`. */
2
- import { Card, SegmentedControl, Typography } from "prime-ui-kit";
1
+ /** A chart widget: a header with a short description and a period switch, and a Sparkline with the period total over its chart — `Card.Header`, `Card.Description`, `Card.Body`. */
2
+ import { Card, SegmentedControl, Sparkline, type SparklinePoint } from "prime-ui-kit";
3
+ import * as React from "react";
3
4
 
4
5
  import styles from "./examples.module.css";
5
6
 
6
- const POINTS = [42, 48, 45, 60, 58, 72, 66, 80, 76, 88, 84, 96];
7
+ type Period = "week" | "month";
7
8
 
8
- /** An area chart path in a 100 × 100 box; the SVG stretches over the whole `Card.Media`. */
9
- const LINE = POINTS.map(
10
- (value, i) => `${i === 0 ? "M" : "L"}${(i * 100) / (POINTS.length - 1)} ${100 - value}`,
11
- ).join(" ");
9
+ const REVENUE: Record<Period, SparklinePoint[]> = {
10
+ week: [
11
+ { label: "6 окт", value: 412_000 },
12
+ { label: "7 окт", value: 468_000 },
13
+ { label: "8 окт", value: 445_000 },
14
+ { label: "9 окт", value: 603_000 },
15
+ { label: "10 окт", value: 581_000 },
16
+ { label: "11 окт", value: 214_000 },
17
+ { label: "12 окт", value: 187_000 },
18
+ ],
19
+ month: [
20
+ { label: "сен, 1 нед.", value: 2_480_000 },
21
+ { label: "сен, 2 нед.", value: 2_760_000 },
22
+ { label: "сен, 3 нед.", value: 2_610_000 },
23
+ { label: "сен, 4 нед.", value: 3_140_000 },
24
+ { label: "окт, 1 нед.", value: 2_910_000 },
25
+ ],
26
+ };
27
+
28
+ /** The same total for the period before: the badge at rest compares the headline with it. */
29
+ const PREVIOUS: Record<Period, number> = {
30
+ week: 2_640_000,
31
+ month: 12_900_000,
32
+ };
33
+
34
+ const formatRubles = (value: number) => `${value.toLocaleString("ru-RU")} ₽`;
12
35
 
13
36
  export default function CardPanelChartExample() {
37
+ const [period, setPeriod] = React.useState<Period>("month");
38
+
14
39
  return (
15
40
  <div className={styles.panel}>
16
41
  <Card.Root variant="panel">
17
42
  <Card.Header>
18
43
  <Card.Title>Выручка</Card.Title>
19
44
  <Card.Description>Оплаченные счета, без возвратов</Card.Description>
20
- <SegmentedControl.Root size="xs" defaultValue="month" aria-label="Период">
45
+ <SegmentedControl.Root
46
+ size="xs"
47
+ value={period}
48
+ onValueChange={(value) => setPeriod(value as Period)}
49
+ aria-label="Период"
50
+ >
21
51
  <SegmentedControl.Item value="week">Неделя</SegmentedControl.Item>
22
52
  <SegmentedControl.Item value="month">Месяц</SegmentedControl.Item>
23
53
  </SegmentedControl.Root>
24
54
  </Card.Header>
25
55
  <Card.Body>
26
- <Typography variant="body-s" tone="secondary">
27
- С начала квартала: 12,6 млн ₽, план выполнен на 84%.
28
- </Typography>
56
+ <Sparkline
57
+ data={REVENUE[period]}
58
+ label="Оплачено за период"
59
+ summary="sum"
60
+ compareTo={PREVIOUS[period]}
61
+ formatValue={formatRubles}
62
+ />
29
63
  </Card.Body>
30
- <Card.Media>
31
- <svg
32
- className={styles.chart}
33
- viewBox="0 0 100 100"
34
- preserveAspectRatio="none"
35
- aria-hidden="true"
36
- >
37
- <path className={styles.chartFill} d={`${LINE} L100 100 L0 100 Z`} />
38
- <path className={styles.chartLine} d={LINE} />
39
- </svg>
40
- </Card.Media>
41
64
  </Card.Root>
42
65
  </div>
43
66
  );
@@ -33,7 +33,7 @@ ChartFrame.Root <figure>; surface of the depth ladder; state, serie
33
33
  ├─ ChartFrame.Plot <div>; fixed height, measured; Crossfade of plot / Skeleton / empty / error
34
34
  ├─ ChartFrame.Legend <ul>; a toggle button per series: swatch · name · value
35
35
  ├─ ChartFrame.Table <table>; visually hidden data, ready state only
36
- └─ ChartFrame.Tooltip <div>; rendered by the chart library's tooltip hook, inside the Root
36
+ └─ ChartFrame.Tooltip <div>; with `point` portaled beside that point of the plot, else in place for a library's tooltip hook
37
37
  ```
38
38
 
39
39
  ## API
@@ -104,7 +104,7 @@ ChartFrame.Root <figure>; surface of the depth ladder; state, serie
104
104
 
105
105
  | Prop | Type | Default | Description |
106
106
  |---|---|---|---|
107
- | `height` | `number` | `240, 160 under 30rem` | Height of the plot area in px. |
107
+ | `height` | `number \| "fill"` | `240, 160 under 30rem` | Height of the plot area in px. `fill` takes the free height of a stretched panel (a `PageContent.Cell` with `rows`), the default height at least. |
108
108
  | `renderPlot` | `(area: ChartFramePlotArea) => ReactNode` | — | Renders the chart at the measured `{ width, height }` with every series `{ id, label, color, hidden }` — for libraries that need explicit sizes (Chart.js, visx). |
109
109
  | `children` | `ReactNode` | — | A chart that sizes itself to its container (an SVG with a `viewBox`, `ResponsiveContainer`); it fills the area. |
110
110
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className` and the other div attributes. |
@@ -118,12 +118,13 @@ ChartFrame.Root <figure>; surface of the depth ladder; state, serie
118
118
  | `…rest` | `HTMLAttributes<HTMLUListElement>` | — | `className` and the other ul attributes. |
119
119
 
120
120
  ### ChartFrame.Tooltip
121
- `ref` → `HTMLDivElement`. The kit's tooltip body for a chart library's custom tooltip (Recharts `content`, visx `TooltipWithBounds`). The library positions it; render it inside `ChartFrame.Root`.
121
+ `ref` → `HTMLDivElement`. The kit's chart tooltip: with `point` it floats beside that point of the plot; without it it is the body for a library's tooltip hook (Recharts `content`). Inside `ChartFrame.Root`.
122
122
 
123
123
  | Prop | Type | Default | Description |
124
124
  |---|---|---|---|
125
125
  | `label` | `string` | — | The point the rows belong to («Неделя 41»). |
126
126
  | `items` | `ChartFrameTooltipItem[]` | — (required) | `{ id, value }` per series at that point; name and color come from the root's series, rows of hidden series are left out, values go through `formatValue`. |
127
+ | `point` | `{ x: number; y: number } \| null` | — | A point in `renderPlot` coordinates (the pointer, or the point the arrows reached): the tooltip floats right of it, left only without room; `null` hides it. Floating, it is `aria-hidden`. |
127
128
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className` and the other div attributes. |
128
129
 
129
130
  ### ChartFrame.Table
@@ -177,7 +178,7 @@ ChartFrame.Root <figure>; surface of the depth ladder; state, serie
177
178
  - The header puts the title and the description in the first column and the actions at the end; under 24rem of panel width the actions drop under the description.
178
179
  - The plot is 240 px high (160 px under 30rem of panel width) unless `height` is set; it never changes height between states.
179
180
  - Legend entries wrap onto more lines; each has a 44 px touch hit area on coarse pointers.
180
- - `ChartFrame.Tooltip` is a floating panel with the point's label over rows of swatch · series name · value; the chart library positions it (Recharts `content`, visx `TooltipWithBounds`, a hand-drawn overlay).
181
+ - `ChartFrame.Tooltip` is the floating layer of the ladder with the point's label over rows of swatch · series name · value. With `point` the kit places it like every anchored layer (`usePosition`): right of the point at its height, on the left only where the right has no room in the viewport, inside the viewport, following the plot on scroll. Without `point` the chart library positions it (Recharts `content`).
181
182
  - SVG `<text>` in the plot takes the caption type in `--prime-color-text-muted`; a class of your own still wins.
182
183
  - Works from 320 px; the container queries are on the panel's own width, so it adapts in a grid cell as on a phone.
183
184
 
@@ -195,6 +196,7 @@ ChartFrame.Root <figure>; surface of the depth ladder; state, serie
195
196
  - The root is a `<figure>` named by `ChartFrame.Title` and described by `ChartFrame.Description`; `aria-busy` while loading.
196
197
  - A legend entry is a button with `aria-pressed` — pressed while its series is shown; the legend list is named by `labels.legend`. A hidden series differs by the swatch's shape (a ring) and a quiet name, never by color alone.
197
198
  - `ChartFrame.Table` is a visually hidden table with the chart's data, named by the title: a row header per point, a column per shown series, values through `formatValue`.
199
+ - A `ChartFrame.Tooltip` with `point` is `aria-hidden` and passive (no focus, no presses; `Escape` hides it until the point moves): it repeats what the plot speaks — give a focusable plot an `aria-valuetext`, like the slider in the tooltip example.
198
200
  - The empty state is `role="status"`, the error `role="alert"` with the retry button.
199
201
  - The drawing is the library's: hide a decorative SVG (`aria-hidden`) when `ChartFrame.Table` carries the data; marks you make focusable need their own `aria-label`.
200
202
 
@@ -213,7 +215,7 @@ ChartFrame.Root <figure>; surface of the depth ladder; state, serie
213
215
  | Example | Shows |
214
216
  |---|---|
215
217
  | [overview.tsx](examples/overview.tsx) | Revenue by month as a hand-drawn SVG line chart in a dashboard panel: the headline with its change, a period switch, a legend that turns series off and a table for screen readers — `series`, `formatValue`, `renderPlot`. |
216
- | [tooltip.tsx](examples/tooltip.tsx) | Invoices by week as grouped bars: pointing at a week or stepping with the arrows shows the kit tooltip body with a row per series — `ChartFrame.Tooltip`, `items`. |
218
+ | [tooltip.tsx](examples/tooltip.tsx) | Invoices by week as grouped bars: the kit tooltip follows the pointer with a row per series, the arrows step through the weeks — `ChartFrame.Tooltip`, `items`, `point`. |
217
219
  | [states.tsx](examples/states.tsx) | Loading, empty, error and data swap in the plot's place without moving the panel; the error retries — `state`, `onRetry`. |
218
220
  | [controlled.tsx](examples/controlled.tsx) | Lead sources with totals in the legend; the hidden series live in the page state and one button shows them all again — `hiddenSeries`, `onHiddenSeriesChange`. |
219
221
  | [narrow.tsx](examples/narrow.tsx) | A 320px column: the actions drop under the title, the plot gets lower and a self-sizing SVG stretches to it — `children`, `height`. |
@@ -249,7 +251,7 @@ ChartFrame.Root <figure>; surface of the depth ladder; state, serie
249
251
  <ChartFrame.Table rows={tableRows} />
250
252
  </ChartFrame.Root>
251
253
  ```
252
- ECharts and Chart.js take the same `width`, `height` and `series[i].color`; their HTML tooltips are strings, so use their own tooltip there or a React overlay with `ChartFrame.Tooltip`.
254
+ ECharts and Chart.js take the same `width`, `height` and `series[i].color`; their HTML tooltips are strings, so use their own tooltip there or `ChartFrame.Tooltip` with `point` from their hover event.
253
255
 
254
256
  ## Mistakes
255
257
  - Colors picked per chart (`#3b82f6`) → `series[i].color` from `renderPlot`, or `chartColors`; status tones (`success`, `danger`) never paint categories.
@@ -259,7 +261,8 @@ ECharts and Chart.js take the same `width`, `height` and `series[i].color`; thei
259
261
  - A legend for a single series → the title already names it; leave `ChartFrame.Legend` out.
260
262
  - Two y-axes of different measures in one frame → two ChartFrames side by side.
261
263
  - The data only in the drawing → add `ChartFrame.Table`; screen readers cannot read a plot.
264
+ - A hand-drawn chart with its tooltip placed by hand (a `foreignObject`, clamped coordinates) → `ChartFrame.Tooltip` with `point`: it flips only when out of room and never jumps at the middle.
262
265
 
263
266
  ## Related
264
- - **Built from:** [Crossfade](../crossfade/COMPONENT.md) (state swap), [Skeleton](../skeleton/COMPONENT.md) (loading), [EmptyPage](../empty-page/COMPONENT.md) (empty, error, retry), `Icon`, `chartColors`
267
+ - **Built from:** [Crossfade](../crossfade/COMPONENT.md) (state swap), [Skeleton](../skeleton/COMPONENT.md) (loading), [EmptyPage](../empty-page/COMPONENT.md) (empty, error, retry), `Icon`, `chartColors`, `ChartTooltip` (the chart tooltip shared with Sparkline)
265
268
  - **See also:** [Sparkline](../sparkline/COMPONENT.md), [Card](../card/COMPONENT.md), [ProgressBar](../progress-bar/COMPONENT.md), [DataTable](../data-table/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md)
@@ -1,4 +1,4 @@
1
- /** Invoices by week as grouped bars: pointing at a week or stepping with the arrows shows the kit tooltip body with a row per series — `ChartFrame.Tooltip`, `items`. */
1
+ /** Invoices by week as grouped bars: the kit tooltip follows the pointer with a row per series, the arrows step through the weeks — `ChartFrame.Tooltip`, `items`, `point`. */
2
2
  import { ChartFrame, type ChartFrameSeries, chartColors } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
@@ -18,12 +18,13 @@ const WEEKS = [
18
18
 
19
19
  const MARGIN = { top: 8, right: 0, bottom: 24, left: 32 };
20
20
  const TICKS = [0, 20, 40, 60];
21
- const TOOLTIP = { width: 176, height: 88 };
22
21
 
23
22
  const formatCount = (value: number) => `${value} шт.`;
24
23
 
25
24
  export default function ChartFrameTooltipExample() {
26
25
  const [active, setActive] = React.useState<number | null>(null);
26
+ // The pointer in plot coordinates; `null` while the arrow keys step through the weeks.
27
+ const [pointer, setPointer] = React.useState<{ x: number; y: number } | null>(null);
27
28
 
28
29
  return (
29
30
  <ChartFrame.Root series={SERIES} formatValue={formatCount}>
@@ -39,13 +40,11 @@ export default function ChartFrameTooltipExample() {
39
40
  const bar = Math.min(24, (band - 16) / series.length);
40
41
  const y = (v: number) => MARGIN.top + innerH * (1 - v / TICKS[TICKS.length - 1]);
41
42
  const point = active === null ? null : WEEKS[active];
42
- const tooltipX =
43
+ // Beside the pointer; from the keyboard beside the middle of the active week.
44
+ const tooltipAt =
43
45
  active === null
44
- ? 0
45
- : Math.min(
46
- Math.max(MARGIN.left + band * active + band / 2 - TOOLTIP.width / 2, 0),
47
- width - TOOLTIP.width,
48
- );
46
+ ? null
47
+ : (pointer ?? { x: MARGIN.left + band * active + band / 2, y: height / 2 });
49
48
  return (
50
49
  <div
51
50
  role="slider"
@@ -58,6 +57,7 @@ export default function ChartFrameTooltipExample() {
58
57
  onPointerMove={(event) => {
59
58
  const rect = event.currentTarget.getBoundingClientRect();
60
59
  const index = Math.floor((event.clientX - rect.left - MARGIN.left) / band);
60
+ setPointer({ x: event.clientX - rect.left, y: event.clientY - rect.top });
61
61
  setActive(index >= 0 && index < WEEKS.length ? index : null);
62
62
  }}
63
63
  onPointerLeave={() => setActive(null)}
@@ -67,6 +67,7 @@ export default function ChartFrameTooltipExample() {
67
67
  const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
68
68
  if (!step) return;
69
69
  event.preventDefault();
70
+ setPointer(null);
70
71
  setActive((current) =>
71
72
  Math.min(Math.max((current ?? 0) + step, 0), WEEKS.length - 1),
72
73
  );
@@ -124,24 +125,19 @@ export default function ChartFrameTooltipExample() {
124
125
  </g>
125
126
  );
126
127
  })}
127
- {point ? (
128
- <foreignObject
129
- x={tooltipX}
130
- y={0}
131
- width={TOOLTIP.width}
132
- height={TOOLTIP.height}
133
- overflow="visible"
134
- >
135
- <ChartFrame.Tooltip
136
- label={point.label}
137
- items={[
128
+ </svg>
129
+ <ChartFrame.Tooltip
130
+ point={tooltipAt}
131
+ label={point?.label}
132
+ items={
133
+ point
134
+ ? [
138
135
  { id: "paid", value: point.paid },
139
136
  { id: "pending", value: point.pending },
140
- ]}
141
- />
142
- </foreignObject>
143
- ) : null}
144
- </svg>
137
+ ]
138
+ : []
139
+ }
140
+ />
145
141
  </div>
146
142
  );
147
143
  }}
@@ -97,7 +97,7 @@ Chat.Root <div> column, fills its host; owns selection,
97
97
  | `search` | `string \| null` | — | The search query (controlled); `null` — the search bar is closed. Matches in the messages' text and transcripts are marked; the newest is current. |
98
98
  | `defaultSearch` | `string \| null` | `null` | The initial search (uncontrolled). |
99
99
  | `onSearchChange` | `(search: string \| null) => void` | — | The search opened (`""`), changed its query or closed (`null`). |
100
- | `current` | `string \| null` | `null` | A message to bring into view (a link, a notification): on change the chat brings it to the middle and the other messages blink back. The focus stays where it is. |
100
+ | `current` | `string \| null` | `null` | A message to bring into view (a link, a notification): on change the chat brings it to the middle and the message itself blinks. The focus stays where it is. |
101
101
  | `typing` | `boolean` | `false` | The other side is typing: `Chat.Description` cross-fades into «печатает…» and the list ends with the typing bubble (`Message.Typing`), which gives way at once to the reply that ends it. |
102
102
  | `labels` | `Partial<ChatLabels>` | — | System strings, see Labels. |
103
103
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "defaultValue">` | — | `className` and the other attributes of the frame. |
@@ -248,7 +248,7 @@ Chat.Root <div> column, fills its host; owns selection,
248
248
  | away from the end | the reader scrolled up | a round soft «к последним» button at the bottom-end, rising in (`fast`); a `Badge` counts incoming messages that came meanwhile; a press glides to the end — from far away it lands one view before the end first — and stops when the reader scrolls; at the end it leaves and the count resets |
249
249
  | first load | `Chat.Messages loading` | the messages' region is `aria-busy`: six `Skeleton` bubbles in the message geometry (the bubble's radius, one or two lines of body text in its padding, widths within its cap), on alternating sides at the end of the list, a group chat's incoming ones beside a circle in the avatar's gutter; `labels.loading` is announced politely. When it ends they cross-fade into the list (`Crossfade`: the new state in `base`, the placeholders out in `fast`); the list mounts then, so what it shows is its first render — nothing enters, nothing is reported new |
250
250
  | older history | `hasOlder` | a bubble-shaped `Skeleton` above the first message; reaching it calls `onLoadOlder`; the rows that arrive above keep the reader's place |
251
- | jumped to | `Message.Reply target`, `Chat.Pinned`, `current` | the list brings the message to the middle (a near one glides, a far one lands at once — rendering it first — and settles once its neighbours have laid out); nothing is drawn on it: every other message steps back and comes forward three times (opacity 1 → `--prime-message-dim-opacity` → 1, `base` each) — `data-pointing="a"` / `"b"` on the list, flipped on every jump so a repeated jump restarts it, and `data-pointed` on the message; a reply's target also takes the focus, a `current` change leaves it where it is. While the search is open its current match takes the place of `current`. Reduced motion: the others stand back for a moment, no blink |
251
+ | jumped to | `Message.Reply target`, `Chat.Pinned`, `current` | the list brings the message to the middle (a near one glides, a far one lands at once — rendering it first — and settles once its neighbours have laid out); the message itself steps back and comes forward three times, the others stay still (opacity 1 → `--prime-message-dim-opacity` → 1, `base` each) — `data-pointing="a"` / `"b"` on the list, flipped on every jump so a repeated jump restarts it, and `data-pointed` on the message; a reply's target also takes the focus, a `current` change leaves it where it is. While the search is open its current match takes the place of `current`. Reduced motion: no blink, the others stand back for a moment |
252
252
  | search | `search` (the bar open) | every case-insensitive match in the messages' text and transcripts under a soft accent wash (`<mark>`); the newest match is current: brought into view while the others stay back (`data-pointing="current"`); Enter and the older / newer buttons move the full strength to the older or newer match in one step; «2 из 5» or «Нет совпадений» beside the field |
253
253
  | typing | `typing` | the description cross-fades into «печатает…» in the accent (`Crossfade`); the typing bubble enters at the end of the list like an incoming message and leaves closing its height |
254
254
  | pinned | `Chat.Pinned items` | the rail's current segment in the accent; a press jumps to the message and steps the bar to the older one (the oldest wraps to the newest), its line entering; without items the bar renders nothing |
@@ -320,7 +320,7 @@ Chat.Root <div> column, fills its host; owns selection,
320
320
  | [structure.tsx](examples/structure.tsx) | The list alone, written as parts: `Chat.Root` with only `Chat.Messages`, a date that stays on top while its day scrolls, an event of the chat and the unread band — `Chat.Messages`, `Chat.Separator`, `Chat.Service`, `Chat.Unread`. |
321
321
  | [group-chat.tsx](examples/group-chat.tsx) | A team chat from data: the first message of an author's run opens with the name in the author's hue, a crown for the owner and the member's tag; the avatar — a photo, or initials — closes the run; a direct chat shows neither — `type`, `items`, `renderMessage`, `Message.Author`, `tag`, `Message.AuthorIcon`, `hueOf`. |
322
322
  | [live.tsx](examples/live.tsx) | A live support chat: the client types — the header and the end of the list say so — then answers; new messages enter at the end, read receipts come from what is on screen, and scrolled up the round button counts what arrived — `typing`, `onNewMessage`, `onRead`. |
323
- | [search.tsx](examples/search.tsx) | Search and pinned messages: the search bar marks every match in the texts and transcripts, counts them and steps between them while the others step back; a pinned message's press jumps to it and blinks the others — `Chat.SearchButton`, `Chat.Search`, `defaultSearch`, `Chat.Pinned`. |
323
+ | [search.tsx](examples/search.tsx) | Search and pinned messages: the search bar marks every match in the texts and transcripts, counts them and steps between them while the others step back; a pinned message's press jumps to it and blinks it — `Chat.SearchButton`, `Chat.Search`, `defaultSearch`, `Chat.Pinned`. |
324
324
  | [states.tsx](examples/states.tsx) | The first load of a conversation: bubble-shaped placeholders on both sides stand in the list's place, then cross-fade into the messages — `loading`. |
325
325
  | [controlled.tsx](examples/controlled.tsx) | Selection mode owned by the screen: the Select item of a message's menu starts it, a check stands beside every message, a press toggles one while the others step back, the bar counts and acts, Escape leaves — `selected`, `onSelectedChange`, `Chat.SelectionBar`. |
326
326
  | [narrow.tsx](examples/narrow.tsx) | At phone width the header keeps its actions and lets the title ellipsize, bubbles take up to 85% of the chat, long links wrap and an album keeps its layout — `Chat.Root`, `Chat.Header`, `Chat.Messages`, `Chat.Footer`. |
@@ -1,4 +1,4 @@
1
- /** Search and pinned messages: the search bar marks every match in the texts and transcripts, counts them and steps between them while the others step back; a pinned message's press jumps to it and blinks the others — `Chat.SearchButton`, `Chat.Search`, `defaultSearch`, `Chat.Pinned`. */
1
+ /** Search and pinned messages: the search bar marks every match in the texts and transcripts, counts them and steps between them while the others step back; a pinned message's press jumps to it and blinks it — `Chat.SearchButton`, `Chat.Search`, `defaultSearch`, `Chat.Pinned`. */
2
2
  import { Card, Chat, Message, VoicePlayer } from "prime-ui-kit";
3
3
 
4
4
  import styles from "./examples.module.css";
@@ -170,7 +170,7 @@ Root attribute: `data-size`. Every motion honours reduced motion (durations coll
170
170
 
171
171
  ## Layout & spacing
172
172
  - The root is a flex column that fills its host's height: put it straight into a `Card.Root` or a `PageContent.List` pane with a height; the scroller takes the rest under the search field.
173
- - The search field stands on 8px sides; the rows on 8px sides and 4px between them (a margin inside each `<li>`, so the virtual window measures rows with their space).
173
+ - One inset all round, the panel's 4px: the search field and the rows stand 4px from every edge of the host, so in a card (radius 12) their 8px corners follow the card's (nested radius = outer − padding). Rows stand 4px apart (a margin inside each `<li>`, so the virtual window measures rows with their space); the last row's margin is the inset below it.
174
174
  - A row: avatar · name and preview · time over the counts. The name and the preview truncate; glyphs, the time and the counts never do.
175
175
  - From 100 dialogs only the rows around the view render; spacers hold the rest of the height. The row with the Tab stop and the row with an open menu stay rendered.
176
176
  - The root is the `chat-list` container: below 240px the preview line and the time step out, the names and counts stay. At 320px (a phone) everything shows.
@@ -10,8 +10,19 @@ const CITIES = [
10
10
  { value: "kzn", label: "Казань" },
11
11
  ];
12
12
 
13
+ /** One tab stop: arrows move the focused row (wrapping), Home / End jump to the ends. */
14
+ function nextIndex(key: string, index: number, count: number): number | null {
15
+ if (key === "ArrowDown") return (index + 1) % count;
16
+ if (key === "ArrowUp") return (index - 1 + count) % count;
17
+ if (key === "Home") return 0;
18
+ if (key === "End") return count - 1;
19
+ return null;
20
+ }
21
+
13
22
  export default function CheckboxIndicatorExample() {
14
23
  const [selected, setSelected] = React.useState<string[]>(["msk"]);
24
+ const [active, setActive] = React.useState(0);
25
+ const optionRefs = React.useRef<(HTMLDivElement | null)[]>([]);
15
26
 
16
27
  const toggle = (value: string) =>
17
28
  setSelected((current) =>
@@ -24,16 +35,27 @@ export default function CheckboxIndicatorExample() {
24
35
  aria-label="Города доставки"
25
36
  aria-multiselectable
26
37
  className={styles.optionList}
38
+ onKeyDown={(event) => {
39
+ const next = nextIndex(event.key, active, CITIES.length);
40
+ if (next === null) return;
41
+ event.preventDefault();
42
+ setActive(next);
43
+ optionRefs.current[next]?.focus();
44
+ }}
27
45
  >
28
- {CITIES.map((city) => {
46
+ {CITIES.map((city, index) => {
29
47
  const isSelected = selected.includes(city.value);
30
48
  return (
31
49
  <div
32
50
  key={city.value}
51
+ ref={(node) => {
52
+ optionRefs.current[index] = node;
53
+ }}
33
54
  role="option"
34
55
  aria-selected={isSelected}
35
- tabIndex={0}
56
+ tabIndex={index === active ? 0 : -1}
36
57
  className={styles.option}
58
+ onFocus={() => setActive(index)}
37
59
  onClick={() => toggle(city.value)}
38
60
  onKeyDown={(event) => {
39
61
  if (event.key === " " || event.key === "Enter") {
@@ -270,7 +270,7 @@ There is no `disabled` on `ColorPicker.Root` — disable parts individually. Con
270
270
  - Field + trigger row: gap `--prime-space-2`, `align-items: flex-end` so the trigger aligns with the field box under a label.
271
271
  - Panel parts gap: 12px (`--prime-space-3`) from `ColorPicker.Panel`.
272
272
  - ColorPresets.Content `align="auto"`: a trigger near the end of the screen lines up the panel's end edge and the panel grows toward the start, and the other way near the start; a fixed `align` still flips at the screen edge.
273
- - ColorPresets grid: one row for up to 8 presets (+ «no color»), else rows of 8 (`columns` overrides); below 480px viewport width the `l` / `xl` grids wrap to half the columns.
273
+ - ColorPresets grid: one row for up to 8 presets (+ «no color»), else rows of 8 (`columns` overrides); below 480px viewport width the `l` / `xl` grids wrap to half the columns. On a coarse pointer every swatch grows to the 44 px touch target and the grid takes at most 5 columns, so it still fits a 320px panel; the arrow keys walk the same columns.
274
274
 
275
275
  ## Accessibility
276
276
 
@@ -306,7 +306,7 @@ There is no `disabled` on `ColorPicker.Root` — disable parts individually. Con
306
306
  | `red` | `"Красный, 0–255"` | Red channel field. |
307
307
  | `green` | `"Зелёный, 0–255"` | Green channel field. |
308
308
  | `blue` | `"Синий, 0–255"` | Blue channel field. |
309
- | `trigger` | `"Цвет"` | ColorPresets trigger name prefix: «<trigger>: <color name>». |
309
+ | `trigger` | `"Цвет"` | ColorPresets trigger name prefix: «<trigger>: <color name>»; in a FieldList cell the column heading. |
310
310
  | `list` | `"Цвета"` | ColorPresets swatch list name when `Content` has no `label`. |
311
311
  | `empty` | `"Без цвета"` | ColorPresets «no color» swatch and the trigger name for an empty value. |
312
312
 
@@ -97,6 +97,7 @@ Controlled with `value` + `onValueChange`; uncontrolled with `defaultValue`.
97
97
 
98
98
  ## Layout & spacing
99
99
  - The swatches take the container width in equal columns: each one stretches across its column (the height stays the tier's). A row wraps only when they no longer fit, and then the swatches split evenly over the rows (16 → 8 + 8, never 14 + 2); no column count to set. In a content-sized container they stand in one row at the tier size.
100
+ - On a coarse pointer the swatches grow to the 44 px touch target (a dense grid cannot grow an invisible hit area past its neighbours) and wrap into more rows.
100
101
  - The keyboard focus ring sits outside the swatches; kit hosts (Modal / Drawer body, panels, cards) already keep `--prime-focus-space` padding, so the ring is never clipped. In your own `overflow` container, keep that padding.
101
102
  - Label → swatches and swatches → hint use the tier `label-gap` / `hint-gap`; field → field 20.
102
103
 
@@ -156,6 +156,7 @@ Committing: without `footer` a picked day (or the second day of a range) applies
156
156
  - Label → field: the tier `label-gap`; field → field in a form: `--prime-space-5`.
157
157
  - `align="auto"`: a field near the end of the screen lines up the popover's end edge and the panel grows toward the start, and the other way near the start; a fixed `align` still flips at the screen edge.
158
158
  - Months gap `--prime-space-6`. `Datepicker.Panel` has no fill, padding or radius — the host is its container; it shows 1–2 months by the host's width with day columns spread across it. An embedded panel measures its parent; narrower than one month → compact cells and footer fields above the buttons. Works from 320 px.
159
+ - On a coarse pointer the day cells grow to the 44 px touch target (a dense grid cannot grow an invisible hit area past its neighbours); narrower than seven of them, the columns shrink.
159
160
  - Values are wall-clock local dates; convert time zones in the app.
160
161
 
161
162
  ## Accessibility
@@ -36,7 +36,7 @@ Dnd.Root session owner: overlay (portal), live region, l
36
36
  ├─ Dnd.Draggable an item to carry elsewhere; stays in place, faded, until the drop
37
37
  └─ Dnd.DropZone a box that takes dropped items
38
38
  ```
39
- A drag in flight is drawn by `Dnd.Root` as a clone of the lifted element above every layer, following the pointer; an item without a fill of its own flies on the fill of the surface it was lifted from (`--prime-color-layer-current`). A refusal with a reason shows the reason in a chip under the clone. The page gets `data-dnd-active="mouse | touch | pen"` on `<html>` for the length of the gesture.
39
+ A drag in flight is drawn by `Dnd.Root` as a clone of the lifted element above every layer, following the pointer. The clone flies inside empty copies (`display: contents`) of its ancestors up to the nearest surface (`data-depth`) and that surface's `data-theme`, so the host's variables and selectors (a List row's tier padding and radius) dress it exactly as the item in place, and it sets down without a jump; an item without a fill of its own flies on the fill of the surface it was lifted from (`--prime-color-layer-current`). A refusal with a reason shows the reason in a chip under the clone. The page gets `data-dnd-active="mouse | touch | pen"` on `<html>` for the length of the gesture.
40
40
 
41
41
  ## API
42
42
 
@@ -190,7 +190,7 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
190
190
  - The list gap is `--prime-space-2`; override it with `className` (widen the rule with `[data-axis]` so it wins: `.gallery[data-axis] { gap: … }`).
191
191
  - A grid (`axis="grid"`) is `display: grid`: the host sets the tracks (`grid-template-columns`) and the cells' size; the gap takes a cell, and a carried item resizes to the cell it would land in.
192
192
  - The reason chip stands `--prime-tooltip-offset` under the carried copy, start-aligned, at most `--prime-tooltip-max-width` wide.
193
- - The look of a row is the consumer's: give `Dnd.SortableItem` / `Dnd.Draggable` a fill, a radius and padding on its own class — the clone is portalled to `<body>`, ancestor selectors do not reach it. A table row is the exception: it flies inside empty copies (`display: contents`) of its ancestors up to the nearest surface (`data-depth`), so the table's own cell styles still apply.
193
+ - The look of a row is the consumer's: give `Dnd.SortableItem` / `Dnd.Draggable` a radius on its own class (the gap and the clone's fill take it) and keep the row's look within its ancestors up to the nearest surface (`data-depth`) — the clone flies inside empty copies (`display: contents`) of exactly those, so their variables and selectors still apply. A table row flies in a one-row copy of its table inside them.
194
194
  - A scrollable ancestor under the pointer auto-scrolls within 64 px of its edge (a `ScrollContainer` strip works).
195
195
  - Keep a zone's height stable between states; it only changes fill and outline.
196
196
 
@@ -246,7 +246,7 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
246
246
  - A `Dnd.SortableItem` `id` that differs from `getId(item)` → the item never moves.
247
247
  - Rendering items yourself instead of through `renderItem` → the list cannot draw the gap or the optimistic order.
248
248
  - Using `index` as the id → ids must survive a reorder.
249
- - Styling a row from an ancestor selector (`.list .row`) → the flying clone loses it.
249
+ - Styling a row from an ancestor above the nearest surface (`data-depth`) → the flying clone loses it; keep the row's look on the row and its hosts inside the surface.
250
250
  - Starting a drag from a button inside a row without `handle` → presses on controls are theirs; use `Dnd.Handle`.
251
251
  - A drag-only move with no alternative → add a button or a menu for the same action.
252
252
  - `aria-label` on an item that shows a title and a description → it hides them; point `aria-labelledby` / `aria-describedby` at them.