prime-ui-kit 0.13.0 → 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.
- package/README.md +7 -6
- package/dist/chunks/{chunk-ZSJMNA46.js → chunk-P6ZO37JM.js} +1 -1
- package/dist/chunks/{chunk-Z3WKKAS6.js → chunk-SYEX7OVT.js} +3572 -2865
- package/dist/chunks/{chunk-E6SQBSDF.js → chunk-WXKRUVSE.js} +199 -162
- package/dist/color-picker.js +2 -2
- package/dist/components/banner/Banner.d.ts +5 -2
- package/dist/components/chart-frame/ChartFrame.d.ts +19 -6
- package/dist/components/chat/Chat.d.ts +2 -2
- package/dist/components/chat/ChatPinned.d.ts +2 -2
- package/dist/components/chat/MessageList.d.ts +1 -1
- package/dist/components/chat/chatContext.d.ts +1 -1
- package/dist/components/field-list/FieldList.d.ts +90 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/input/Input.d.ts +2 -2
- package/dist/components/kanban/Kanban.d.ts +6 -3
- package/dist/components/page-content/PageContent.d.ts +55 -4
- package/dist/components/sparkline/Sparkline.d.ts +64 -15
- package/dist/components/sparkline/geometry.d.ts +26 -0
- package/dist/components/tooltip/Tooltip.d.ts +5 -2
- package/dist/hooks/usePosition.d.ts +15 -4
- package/dist/index.css +1 -1
- package/dist/index.js +4 -2
- package/dist/internal/ChartTooltip.d.ts +26 -0
- package/dist/internal/FieldAction.d.ts +6 -7
- package/dist/internal/fieldCell.d.ts +21 -0
- package/dist/internal/messageList.d.ts +2 -2
- package/dist/internal/palette.d.ts +7 -0
- package/dist/skill/SKILL.md +24 -14
- package/dist/skill/anti-slop.md +77 -4
- package/dist/skill/cheatsheet.md +24 -5
- package/dist/skill/checklist.md +19 -1
- package/dist/skill/choosing.md +10 -5
- package/dist/skill/components.md +4 -3
- package/dist/skill/composition.md +28 -15
- package/dist/skill/foundations.md +8 -6
- package/dist/skill/layouts.md +6 -6
- package/dist/skill/patterns/app-shell.module.css +1 -8
- package/dist/skill/patterns/app-shell.tsx +11 -9
- package/dist/skill/patterns/chat.tsx +23 -5
- package/dist/skill/patterns/dashboard.module.css +1 -31
- package/dist/skill/patterns/dashboard.tsx +150 -136
- package/dist/skill/patterns/detail-page.module.css +0 -3
- package/dist/skill/patterns/detail-page.tsx +4 -7
- package/dist/skill/patterns/form-drawer.tsx +2 -2
- package/dist/skill/patterns/kanban-board.tsx +4 -2
- package/dist/skill/patterns/list-page.tsx +2 -2
- package/dist/skill/patterns/media-library.module.css +0 -22
- package/dist/skill/patterns/media-library.tsx +17 -18
- package/dist/skill/patterns/record-editor.module.css +18 -0
- package/dist/skill/patterns/record-editor.tsx +356 -0
- package/dist/skill/patterns/screen-states.module.css +3 -17
- package/dist/skill/patterns/screen-states.tsx +70 -69
- package/dist/skill/patterns/settings-page.module.css +1 -21
- package/dist/skill/patterns/settings-page.tsx +58 -79
- package/dist/skill/reference/components/avatar/COMPONENT.md +4 -4
- package/dist/skill/reference/components/avatar/examples/presence.tsx +1 -1
- package/dist/skill/reference/components/avatar/examples/states.tsx +1 -1
- package/dist/skill/reference/components/banner/COMPONENT.md +6 -6
- package/dist/skill/reference/components/banner/examples/dismissible.tsx +22 -11
- package/dist/skill/reference/components/banner/examples/examples.module.css +3 -3
- package/dist/skill/reference/components/banner/examples/narrow.tsx +1 -1
- package/dist/skill/reference/components/breadcrumb/COMPONENT.md +1 -1
- package/dist/skill/reference/components/breadcrumb/examples/overflow.tsx +1 -1
- package/dist/skill/reference/components/card/COMPONENT.md +5 -4
- package/dist/skill/reference/components/card/examples/examples.module.css +0 -28
- package/dist/skill/reference/components/card/examples/kpi-media.tsx +13 -11
- package/dist/skill/reference/components/card/examples/narrow.tsx +1 -1
- package/dist/skill/reference/components/card/examples/panel-chart.tsx +45 -22
- package/dist/skill/reference/components/chart-frame/COMPONENT.md +10 -7
- package/dist/skill/reference/components/chart-frame/examples/tooltip.tsx +20 -24
- package/dist/skill/reference/components/chat/COMPONENT.md +3 -3
- package/dist/skill/reference/components/chat/examples/search.tsx +1 -1
- package/dist/skill/reference/components/chat-list/COMPONENT.md +1 -1
- package/dist/skill/reference/components/checkbox/examples/indicator.tsx +24 -2
- package/dist/skill/reference/components/color-picker/COMPONENT.md +2 -2
- package/dist/skill/reference/components/color-swatches/COMPONENT.md +1 -0
- package/dist/skill/reference/components/datepicker/COMPONENT.md +1 -0
- package/dist/skill/reference/components/dnd/COMPONENT.md +3 -3
- package/dist/skill/reference/components/field-list/COMPONENT.md +164 -0
- package/dist/skill/reference/components/field-list/examples/base-row.tsx +76 -0
- package/dist/skill/reference/components/field-list/examples/controlled.tsx +72 -0
- package/dist/skill/reference/components/field-list/examples/errors.tsx +85 -0
- package/dist/skill/reference/components/field-list/examples/examples.module.css +34 -0
- package/dist/skill/reference/components/field-list/examples/limits.tsx +55 -0
- package/dist/skill/reference/components/field-list/examples/locked.tsx +139 -0
- package/dist/skill/reference/components/field-list/examples/narrow.tsx +68 -0
- package/dist/skill/reference/components/field-list/examples/overview.tsx +66 -0
- package/dist/skill/reference/components/field-list/examples/reorder.tsx +42 -0
- package/dist/skill/reference/components/field-list/examples/sizes.tsx +42 -0
- package/dist/skill/reference/components/field-list/examples/states.tsx +51 -0
- package/dist/skill/reference/components/input/examples/examples.module.css +2 -7
- package/dist/skill/reference/components/kanban/COMPONENT.md +13 -9
- package/dist/skill/reference/components/kanban/examples/limits.tsx +2 -1
- package/dist/skill/reference/components/kanban/examples/variants.tsx +16 -8
- package/dist/skill/reference/components/list/COMPONENT.md +1 -1
- package/dist/skill/reference/components/media-viewer/COMPONENT.md +2 -2
- package/dist/skill/reference/components/message/COMPONENT.md +2 -2
- package/dist/skill/reference/components/notification/COMPONENT.md +1 -0
- package/dist/skill/reference/components/page-content/COMPONENT.md +70 -14
- package/dist/skill/reference/components/page-content/examples/grid.tsx +107 -0
- package/dist/skill/reference/components/page-content/examples/overview.tsx +2 -2
- package/dist/skill/reference/components/page-content/examples/sections.tsx +81 -0
- package/dist/skill/reference/components/page-toolbar/COMPONENT.md +1 -1
- package/dist/skill/reference/components/segmented-control/COMPONENT.md +1 -0
- package/dist/skill/reference/components/select/COMPONENT.md +4 -2
- package/dist/skill/reference/components/select/examples/icon-only.tsx +38 -0
- package/dist/skill/reference/components/sparkline/COMPONENT.md +83 -30
- package/dist/skill/reference/components/sparkline/examples/compact.tsx +1 -1
- package/dist/skill/reference/components/sparkline/examples/curve.tsx +28 -0
- package/dist/skill/reference/components/sparkline/examples/gaps.tsx +33 -0
- package/dist/skill/reference/components/sparkline/examples/overview.tsx +11 -2
- package/dist/skill/reference/components/sparkline/examples/period.tsx +7 -2
- package/dist/skill/reference/components/sparkline/examples/reference.tsx +37 -0
- package/dist/skill/reference/components/sparkline/examples/variants.tsx +2 -2
- package/dist/skill/reference/components/tabs/examples/examples.module.css +0 -6
- package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +11 -13
- package/dist/skill/reference/components/timeline/examples/examples.module.css +0 -8
- package/dist/skill/reference/components/timeline/examples/narrow.tsx +2 -2
- package/dist/skill/reference/components/timeline/examples/sizes.tsx +3 -3
- package/dist/skill/reference/components/timeline/examples/variants.tsx +4 -4
- package/dist/skill/reference/components/tooltip/COMPONENT.md +6 -4
- package/dist/skill/reference/components/tooltip/examples/placement.tsx +2 -1
- package/dist/skill/reference/components/video-note/COMPONENT.md +1 -1
- package/dist/skill/reference/components/voice-player/COMPONENT.md +1 -1
- package/dist/skill/reference/layout/app-header/examples/in-app-shell.tsx +2 -2
- package/dist/skill/reference/layout/app-shell/COMPONENT.md +1 -1
- package/dist/skill/reference/layout/app-shell/examples/footer.tsx +2 -2
- package/dist/skill/reference/layout/app-shell/examples/overview.tsx +2 -2
- package/dist/skill/reference/layout/app-shell/examples/template.tsx +2 -2
- package/dist/skill/reference/layout/bottom-nav/examples/floating-action.tsx +2 -2
- package/dist/skill/reference/layout/bottom-nav/examples/floating.tsx +2 -2
- package/dist/skill/reference/layout/bottom-nav/examples/in-app-shell.tsx +2 -2
- package/dist/skill/responsive.md +5 -3
- package/dist/skill/ux.md +145 -0
- package/dist/tokens/semantic.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/avatar/COMPONENT.md +4 -4
- package/src/components/avatar/examples/presence.tsx +1 -1
- package/src/components/avatar/examples/states.tsx +1 -1
- package/src/components/banner/COMPONENT.md +6 -6
- package/src/components/banner/examples/dismissible.tsx +22 -11
- package/src/components/banner/examples/examples.module.css +3 -3
- package/src/components/banner/examples/narrow.tsx +1 -1
- package/src/components/breadcrumb/COMPONENT.md +1 -1
- package/src/components/breadcrumb/examples/overflow.tsx +1 -1
- package/src/components/card/COMPONENT.md +5 -4
- package/src/components/card/examples/examples.module.css +0 -28
- package/src/components/card/examples/kpi-media.tsx +13 -11
- package/src/components/card/examples/narrow.tsx +1 -1
- package/src/components/card/examples/panel-chart.tsx +45 -22
- package/src/components/chart-frame/COMPONENT.md +10 -7
- package/src/components/chart-frame/examples/tooltip.tsx +20 -24
- package/src/components/chat/COMPONENT.md +3 -3
- package/src/components/chat/examples/search.tsx +1 -1
- package/src/components/chat-list/COMPONENT.md +1 -1
- package/src/components/checkbox/examples/indicator.tsx +24 -2
- package/src/components/color-picker/COMPONENT.md +2 -2
- package/src/components/color-swatches/COMPONENT.md +1 -0
- package/src/components/datepicker/COMPONENT.md +1 -0
- package/src/components/dnd/COMPONENT.md +3 -3
- package/src/components/field-list/COMPONENT.md +164 -0
- package/src/components/field-list/examples/base-row.tsx +76 -0
- package/src/components/field-list/examples/controlled.tsx +72 -0
- package/src/components/field-list/examples/errors.tsx +85 -0
- package/src/components/field-list/examples/examples.module.css +34 -0
- package/src/components/field-list/examples/limits.tsx +55 -0
- package/src/components/field-list/examples/locked.tsx +139 -0
- package/src/components/field-list/examples/narrow.tsx +68 -0
- package/src/components/field-list/examples/overview.tsx +66 -0
- package/src/components/field-list/examples/reorder.tsx +42 -0
- package/src/components/field-list/examples/sizes.tsx +42 -0
- package/src/components/field-list/examples/states.tsx +51 -0
- package/src/components/input/examples/examples.module.css +2 -7
- package/src/components/kanban/COMPONENT.md +13 -9
- package/src/components/kanban/examples/limits.tsx +2 -1
- package/src/components/kanban/examples/variants.tsx +16 -8
- package/src/components/list/COMPONENT.md +1 -1
- package/src/components/media-viewer/COMPONENT.md +2 -2
- package/src/components/message/COMPONENT.md +2 -2
- package/src/components/notification/COMPONENT.md +1 -0
- package/src/components/page-content/COMPONENT.md +70 -14
- package/src/components/page-content/examples/grid.tsx +107 -0
- package/src/components/page-content/examples/overview.tsx +2 -2
- package/src/components/page-content/examples/sections.tsx +81 -0
- package/src/components/page-toolbar/COMPONENT.md +1 -1
- package/src/components/segmented-control/COMPONENT.md +1 -0
- package/src/components/select/COMPONENT.md +4 -2
- package/src/components/select/examples/icon-only.tsx +38 -0
- package/src/components/sparkline/COMPONENT.md +83 -30
- package/src/components/sparkline/examples/compact.tsx +1 -1
- package/src/components/sparkline/examples/curve.tsx +28 -0
- package/src/components/sparkline/examples/gaps.tsx +33 -0
- package/src/components/sparkline/examples/overview.tsx +11 -2
- package/src/components/sparkline/examples/period.tsx +7 -2
- package/src/components/sparkline/examples/reference.tsx +37 -0
- package/src/components/sparkline/examples/variants.tsx +2 -2
- package/src/components/tabs/examples/examples.module.css +0 -6
- package/src/components/tabs/examples/in-drawer.tsx +11 -13
- package/src/components/timeline/examples/examples.module.css +0 -8
- package/src/components/timeline/examples/narrow.tsx +2 -2
- package/src/components/timeline/examples/sizes.tsx +3 -3
- package/src/components/timeline/examples/variants.tsx +4 -4
- package/src/components/tooltip/COMPONENT.md +6 -4
- package/src/components/tooltip/examples/placement.tsx +2 -1
- package/src/components/video-note/COMPONENT.md +1 -1
- package/src/components/voice-player/COMPONENT.md +1 -1
- package/src/layout/app-header/examples/in-app-shell.tsx +2 -2
- package/src/layout/app-shell/COMPONENT.md +1 -1
- package/src/layout/app-shell/examples/footer.tsx +2 -2
- package/src/layout/app-shell/examples/overview.tsx +2 -2
- package/src/layout/app-shell/examples/template.tsx +2 -2
- package/src/layout/bottom-nav/examples/floating-action.tsx +2 -2
- package/src/layout/bottom-nav/examples/floating.tsx +2 -2
- package/src/layout/bottom-nav/examples/in-app-shell.tsx +2 -2
- package/src/styles/theme-light.css +1 -0
|
@@ -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>;
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
43
|
+
// Beside the pointer; from the keyboard beside the middle of the active week.
|
|
44
|
+
const tooltipAt =
|
|
43
45
|
active === null
|
|
44
|
-
?
|
|
45
|
-
:
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
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);
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# FieldList
|
|
2
|
+
|
|
3
|
+
**Category:** inputs
|
|
4
|
+
**Kind:** composite
|
|
5
|
+
|
|
6
|
+
> An editable list of records of one shape — invoice lines, specs, contacts, prices by period: one row per record, the column headings once above the rows, a remove button per row and an add button under them.
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
- A form part where a person adds, edits and removes several records of the same fields: invoice lines, contacts of a client, specs of a plan, recipients.
|
|
10
|
+
- A base value with overrides (a price and its changes by month): the base is the first row, pinned with `getLocked`, each override is a row.
|
|
11
|
+
- A list of short texts in the order a person sets (selling points, checklist items): one column, `reorderable`.
|
|
12
|
+
- Small attributes of a record beside its text (an icon, a color): `width="compact"` columns with an icon-only `Select` (`Select.TriggerIcon` and no `Select.Value`) and a `ColorPresets` swatch.
|
|
13
|
+
|
|
14
|
+
## When not to use
|
|
15
|
+
- Records that are only read, sorted or filtered → [DataTable](../data-table/COMPONENT.md); its rows are data, not form fields.
|
|
16
|
+
- One value in several units or currencies (a deposit in ₽, $, ฿) → fields side by side in one row of the form, each unit in `Input.InlineAffix`, under one heading; not a list a person grows.
|
|
17
|
+
- A set of tags or values picked from options → [TagSelect](../tag-select/COMPONENT.md).
|
|
18
|
+
- Reordering items that are not form rows (menu sections, cards) → [Dnd](../dnd/COMPONENT.md) or [List](../list/COMPONENT.md) `onReorder`.
|
|
19
|
+
- A record with many fields or long texts per row → a list of records that opens each one in a [Drawer](../drawer/COMPONENT.md); a row holds up to four short fields.
|
|
20
|
+
|
|
21
|
+
## Import
|
|
22
|
+
```tsx
|
|
23
|
+
import { FieldList, type FieldListColumn } from "prime-ui-kit";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Anatomy
|
|
27
|
+
```
|
|
28
|
+
FieldList field frame: label · the list · hint / error
|
|
29
|
+
├─ heading row the column headings, once (aria-hidden; each field carries its heading as its label)
|
|
30
|
+
├─ <ol> the rows (Dnd.Sortable with `reorderable`)
|
|
31
|
+
│ └─ <li> row [lead: handle | lock] [a cell per column] [end: remove]
|
|
32
|
+
│ └─ cell one kit field without `label`, named by its column heading
|
|
33
|
+
└─ add row Button «addLabel» (+ `labels.maxReached` at `max`)
|
|
34
|
+
```
|
|
35
|
+
A leaf: rows come from `value` through `renderItem`, which returns one element per column (a fragment). Every framed kit field (Input, Select, Datepicker, NativeSelect, ColorField…) in a cell takes its column heading as its label: read by screen readers, not shown while the heading row is on screen, shown above the field once the rows stack in a narrow container. `ColorPresets` takes it as the name of its trigger.
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
40
|
+
|
|
41
|
+
### FieldList
|
|
42
|
+
Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editable list of records of one shape: column headings once, a row of fields per record (`<ol>`), remove per row, add under the rows; rows stack under 480 px.
|
|
43
|
+
|
|
44
|
+
| Prop | Type | Default | Description |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| `columns` | `readonly FieldListColumn[]` | — (required) | The columns in cell order: `label` (the heading; it names the field of the column in every row), `width` (`fill` \| `compact` \| `s` \| `m`), `required` (a `*` after the heading). |
|
|
47
|
+
| `renderItem` | `(item: T, row: FieldListRow<T>) => ReactNode` | — (required) | The cells of a row, one element per column (a fragment): kit fields without `label`. `row` has `index`, `update(patch)` and `remove()`. |
|
|
48
|
+
| `getId` | `(item: T) => string` | — (required) | Stable unique id of an item: the row key and the drag id. |
|
|
49
|
+
| `value` | `readonly T[]` | — | The rows (controlled). |
|
|
50
|
+
| `defaultValue` | `readonly T[]` | `[]` | The first rows (uncontrolled). |
|
|
51
|
+
| `onValueChange` | `(value: T[]) => void` | — | The new list after an edit (`row.update`), an added, removed or moved row. |
|
|
52
|
+
| `createItem` | `() => T` | — | A new empty item; with it the add button shows under the rows and focus goes to the new row's first field. |
|
|
53
|
+
| `addLabel` | `string` | — | Text of the add button: what is added («Добавить позицию»). |
|
|
54
|
+
| `getLabel` | `(item: T) => string` | — | The row's own text (its title field) for the remove button, the handle and drag announcements; else the row number. |
|
|
55
|
+
| `getLocked` | `(item: T) => boolean` | — | Pinned rows: a lock in the lead column, no remove button, they hold their place while others move. |
|
|
56
|
+
| `reorderable` | `boolean` | `false` | Rows move by a leading handle: drag, a touch hold, Alt + ↑ / ↓. |
|
|
57
|
+
| `min` | `number` | `0` | The fewest rows: at this count the remove buttons are disabled. |
|
|
58
|
+
| `max` | `number` | — | The most rows: at this count the add button is disabled with `labels.maxReached` beside it. |
|
|
59
|
+
| `label` | `ReactNode` | — | Heading of the list; names the `<ol>`. Omit it inside a `Fieldset` whose legend names the list. |
|
|
60
|
+
| `aria-label` | `string` | — | Name of the `<ol>` when no visible `label` names it (a section heading above the list). |
|
|
61
|
+
| `hint` | `ReactNode` | — | Help text under the list; hidden while `error` shows. |
|
|
62
|
+
| `error` | `ReactNode` | — | A problem of the list as a whole («Добавьте хотя бы одну позицию»); a field's own error goes on that field. |
|
|
63
|
+
| `disabled` | `boolean` | `false` | Disables every field, handle and button; a disabled `Fieldset` around it sets it too. |
|
|
64
|
+
| `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `"m"` | Tier of the headings, buttons and the kit fields that read the host tier. Without it the host tier, else `m`. |
|
|
65
|
+
| `labels` | `Partial<FieldListLabels>` | — | Built-in strings, see Labels. |
|
|
66
|
+
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "id" \| "children" \| "defaultValue" \| "defaultChecked" \| "onChange">` | — | `className`, `data-*` and the other attributes of the field frame `<div>` (field-root rule: `className`, `ref` and the rest → frame, `id` → control). |
|
|
67
|
+
|
|
68
|
+
## Variants
|
|
69
|
+
|
|
70
|
+
### width (column)
|
|
71
|
+
| Value | Looks like | Use when | Default |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| `fill` | shares the free width with the other `fill` columns | names, descriptions, any text | yes |
|
|
74
|
+
| `compact` | two control heights wide | an icon-only `Select`, a `ColorPresets` swatch, a checkbox | |
|
|
75
|
+
| `s` | 144 px | a quantity, a code, a number of days | |
|
|
76
|
+
| `m` | 192 px | an amount with its unit, a date, a phone | |
|
|
77
|
+
|
|
78
|
+
### Flags
|
|
79
|
+
| Value | Looks like | Use when | Default |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| `reorderable` | a grip before every row; Alt + ↑ / ↓ on it | the order of the records matters (a pricing page, a checklist) | `false` |
|
|
82
|
+
| `getLocked` | a lock where the grip is, no remove button; the row holds its place | a base value, system records a person cannot drop | — |
|
|
83
|
+
| `createItem` | the add button under the rows, aligned with the first column | the person grows the list | — |
|
|
84
|
+
| `min` · `max` | remove buttons off at `min`; the add button off with «Не больше {max}» at `max` | the record count is bounded | `0` · — |
|
|
85
|
+
|
|
86
|
+
**Combinations**
|
|
87
|
+
- Recommended: `label` on the list (or a `Fieldset` legend around it), one heading per column, a `getLabel` from the title field, an `addLabel` that says what is added.
|
|
88
|
+
- Allowed but rare: one column — the heading row is not drawn, the list label names the rows.
|
|
89
|
+
- Pointless: `getLocked` on every row with `createItem` — nothing can be removed or moved.
|
|
90
|
+
- Forbidden: a `label` on every field of a row, a numbered label («Пункт 1», «Пункт 2»), a unit in a heading when the field shows it.
|
|
91
|
+
|
|
92
|
+
## States
|
|
93
|
+
| State | Driven by | DOM |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| empty | `value` with no items | no heading row; only the add button (with `createItem`) |
|
|
96
|
+
| at min | `value.length <= min` | remove buttons `disabled` |
|
|
97
|
+
| at max | `value.length >= max` | add button `disabled`, `labels.maxReached` beside it |
|
|
98
|
+
| pinned row | `getLocked(item)` | `data-locked="true"` on the `<li>`; a lock, no remove button |
|
|
99
|
+
| invalid | `error` (the list), a field's own `error` | the list's message under the add row; a field's under the field in its row |
|
|
100
|
+
| disabled | `disabled` (or a disabled Fieldset) | `data-disabled="true"` on the frame; fields, handles and buttons disabled through the kit's disabled context |
|
|
101
|
+
| stacked | container narrower than 480 px | heading row hidden, each row's fields in one column with their labels shown |
|
|
102
|
+
|
|
103
|
+
## Layout & spacing
|
|
104
|
+
- Rows 12 apart (`--prime-space-3`); columns 12 apart; the heading row sits at the tier's label gap above the first row.
|
|
105
|
+
- Every row and the heading row share one track list: `[lead] [columns] [end]`; the lead fits the grip, the end fits a remove button of the tier.
|
|
106
|
+
- The handle, the lock and the remove button stand on the middle of the first line of fields, so a field's error under it grows the row downwards only.
|
|
107
|
+
- The add button lines up with the first column of fields, 12 under the last row.
|
|
108
|
+
- Under 480 px of its own width (a container query): rows stack, fields 16 apart inside a row, rows 24 apart.
|
|
109
|
+
|
|
110
|
+
## Accessibility
|
|
111
|
+
|
|
112
|
+
### Keyboard
|
|
113
|
+
| Key | Action |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `Tab` | Moves through the fields of a row, then to its remove button and on. |
|
|
116
|
+
| `Enter` · `Space` | On the add button adds a row and focuses its first field; on a remove button removes the row and focuses the next row's remove button. |
|
|
117
|
+
| `Alt + ↑` · `Alt + ↓` | With `reorderable`, moves the row up or down (from its handle). |
|
|
118
|
+
|
|
119
|
+
### ARIA
|
|
120
|
+
- The rows are an `<ol>` named by the list `label` (`aria-labelledby`); the list gives each row its position.
|
|
121
|
+
- The heading row is `aria-hidden`: each field in a cell carries its column heading as its own label, read but not shown on wide rows.
|
|
122
|
+
- In a narrow container the heading row goes and each field's label shows above it.
|
|
123
|
+
- The remove button is «Удалить {row}», the handle «Переместить {row}»: the row's name from `getLabel`, else its number.
|
|
124
|
+
- A pinned row has a lock in place of the handle and no remove button; without `reorderable` the lock is an image named «Закреплена».
|
|
125
|
+
|
|
126
|
+
### Labels
|
|
127
|
+
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
128
|
+
|
|
129
|
+
| Key | Default | Used for |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `remove` | `"Удалить {row}"` | `aria-label` of a row's remove button; `{row}` is the row's name. |
|
|
132
|
+
| `reorder` | `"Переместить {row}"` | `aria-label` of a row's handle (`reorderable`); `{row}` is the row's name. |
|
|
133
|
+
| `row` | `"строку {index}"` | Name of a row without `getLabel` text; `{index}` counts from 1. |
|
|
134
|
+
| `namedRow` | `"«{label}»"` | Name of a row with `getLabel` text; `{label}` is that text. |
|
|
135
|
+
| `locked` | `"Закреплена"` | Spoken name of the lock of a pinned row in a list without `reorderable`. |
|
|
136
|
+
| `maxReached` | `"Не больше {max}"` | Text beside the disabled add button once the list holds `max` rows. |
|
|
137
|
+
|
|
138
|
+
## Examples
|
|
139
|
+
| Example | Shows |
|
|
140
|
+
|---|---|
|
|
141
|
+
| [overview.tsx](examples/overview.tsx) | Lines of an invoice: the column headings stand once above the rows, each row has a remove button and the add button sits under them — `columns`, `renderItem`, `createItem`, `addLabel`. |
|
|
142
|
+
| [sizes.tsx](examples/sizes.tsx) | Every tier: the headings, the fields, the handle and the buttons follow the list's size — `size`. |
|
|
143
|
+
| [reorder.tsx](examples/reorder.tsx) | Selling points of a plan in the order of the pricing page: a one-column list has no headings and no numbers, a point moves by its handle or Alt+arrows — `reorderable`, `getLabel`. |
|
|
144
|
+
| [locked.tsx](examples/locked.tsx) | Specs of a plan on one line each: an icon picker and a color swatch take compact columns, the first three specs are pinned with a lock and the rest reorder below them — `getLocked`, `width`, `reorderable`. |
|
|
145
|
+
| [base-row.tsx](examples/base-row.tsx) | A plan price and its changes by month in one table: the base price is the first, pinned row, each change is a row with its month and its price, and the unit sits in the field, not in a heading — `getLocked`, `width`. |
|
|
146
|
+
| [limits.tsx](examples/limits.tsx) | Recipients of invoice reminders, from one to three: the last row cannot be removed and the add button says why it is off once the list is full — `min`, `max`, `hint`. |
|
|
147
|
+
| [errors.tsx](examples/errors.tsx) | Contacts of a client checked on save: a field's problem shows under that field in its row, a problem of the whole list under the list, and focus goes to the first wrong field — `error`. |
|
|
148
|
+
| [states.tsx](examples/states.tsx) | The same list editable and disabled while the request saves it: fields, handles and buttons turn off together — `disabled`. |
|
|
149
|
+
| [controlled.tsx](examples/controlled.tsx) | The parent owns the lines: it sums them under the list and puts the saved lines back on reset — `value`, `onValueChange`. |
|
|
150
|
+
| [narrow.tsx](examples/narrow.tsx) | The invoice lines in a 320 px column: the headings give way, each row stacks its fields with their own labels and rows stand a group apart — `columns`. |
|
|
151
|
+
|
|
152
|
+
## Mistakes
|
|
153
|
+
- A `label` above every field of every row («Иконка», «Параметр», «Значение» four times) → a FieldList: the headings stand once, the fields take them as hidden labels.
|
|
154
|
+
- Numbered labels («Преимущество 1», «Преимущество 2») → none: the list label names the rows and the order is visible.
|
|
155
|
+
- One record split over two lines of half-width fields → one row; small attributes go to `compact` columns (icon-only `Select`, `ColorPresets`).
|
|
156
|
+
- A base value as a separate field above its overrides, in another geometry → the first row, pinned with `getLocked`.
|
|
157
|
+
- The unit in every heading («Цена, ₽ / день») and again in the field → the unit once, in `Input.InlineAffix`.
|
|
158
|
+
- A hint that explains what the lock already shows («Первые три всегда первыми») → no hint; the lock says it.
|
|
159
|
+
- `aria-label` written on every field in a cell → none: the column heading names the field.
|
|
160
|
+
- A grip, a remove button and an add button drawn by hand around a `.map()` of fields → FieldList.
|
|
161
|
+
|
|
162
|
+
## Related
|
|
163
|
+
- **Built from:** [Button](../button/COMPONENT.md) (add, remove), [Dnd](../dnd/COMPONENT.md) (`reorderable`: `Dnd.Sortable`, `Dnd.Handle`), [Label](../label/COMPONENT.md) (headings), `Icon` (lock), the field frame (label, hint, error)
|
|
164
|
+
- **See also:** [Input](../input/COMPONENT.md), [Select](../select/COMPONENT.md) (icon-only trigger), [ColorPicker](../color-picker/COMPONENT.md) (`ColorPresets`), [Fieldset](../fieldset/COMPONENT.md), [DataTable](../data-table/COMPONENT.md)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** A plan price and its changes by month in one table: the base price is the first, pinned row, each change is a row with its month and its price, and the unit sits in the field, not in a heading — `getLocked`, `width`. */
|
|
2
|
+
import { FieldList, type FieldListColumn, Input, Select } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
type Period = { id: string; from: string; price: string; base?: boolean };
|
|
6
|
+
|
|
7
|
+
const COLUMNS: FieldListColumn[] = [{ label: "Действует" }, { label: "Цена за месяц", width: "m" }];
|
|
8
|
+
|
|
9
|
+
const MONTHS = [
|
|
10
|
+
{ value: "01", label: "С 1 января" },
|
|
11
|
+
{ value: "04", label: "С 1 апреля" },
|
|
12
|
+
{ value: "07", label: "С 1 июля" },
|
|
13
|
+
{ value: "10", label: "С 1 октября" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const PERIODS: Period[] = [
|
|
17
|
+
{ id: "base", from: "", price: "4 900", base: true },
|
|
18
|
+
{ id: "q3", from: "07", price: "5 400" },
|
|
19
|
+
{ id: "q4", from: "10", price: "5 900" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export default function FieldListBaseRowExample() {
|
|
23
|
+
const [periods, setPeriods] = React.useState(PERIODS);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<FieldList
|
|
27
|
+
label="Цена тарифа"
|
|
28
|
+
columns={COLUMNS}
|
|
29
|
+
value={periods}
|
|
30
|
+
onValueChange={setPeriods}
|
|
31
|
+
getId={(period) => period.id}
|
|
32
|
+
getLocked={(period) => period.base ?? false}
|
|
33
|
+
createItem={() => ({ id: crypto.randomUUID(), from: "", price: "" })}
|
|
34
|
+
addLabel="Добавить изменение цены"
|
|
35
|
+
renderItem={(period, row) => (
|
|
36
|
+
<>
|
|
37
|
+
{period.base ? (
|
|
38
|
+
<Input.Root>
|
|
39
|
+
<Input.Wrapper>
|
|
40
|
+
<Input.Field value="Всегда, если нет изменений" readOnly />
|
|
41
|
+
</Input.Wrapper>
|
|
42
|
+
</Input.Root>
|
|
43
|
+
) : (
|
|
44
|
+
<Select.Root
|
|
45
|
+
value={period.from}
|
|
46
|
+
onValueChange={(from) => row.update({ from })}
|
|
47
|
+
placeholder="Выберите месяц"
|
|
48
|
+
>
|
|
49
|
+
<Select.Trigger>
|
|
50
|
+
<Select.Value />
|
|
51
|
+
</Select.Trigger>
|
|
52
|
+
<Select.Content>
|
|
53
|
+
{MONTHS.map((month) => (
|
|
54
|
+
<Select.Item key={month.value} value={month.value}>
|
|
55
|
+
{month.label}
|
|
56
|
+
</Select.Item>
|
|
57
|
+
))}
|
|
58
|
+
</Select.Content>
|
|
59
|
+
</Select.Root>
|
|
60
|
+
)}
|
|
61
|
+
<Input.Root>
|
|
62
|
+
<Input.Wrapper>
|
|
63
|
+
<Input.Field
|
|
64
|
+
value={period.price}
|
|
65
|
+
inputMode="decimal"
|
|
66
|
+
placeholder="0"
|
|
67
|
+
onChange={(event) => row.update({ price: event.target.value })}
|
|
68
|
+
/>
|
|
69
|
+
<Input.InlineAffix side="end">₽</Input.InlineAffix>
|
|
70
|
+
</Input.Wrapper>
|
|
71
|
+
</Input.Root>
|
|
72
|
+
</>
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|