prime-ui-kit 0.9.2 → 0.9.3
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 +9 -5
- package/dist/chunks/{chunk-ZEFCDEEQ.js → chunk-6JTZOA5I.js} +878 -331
- package/dist/chunks/{chunk-LXGRKN34.js → chunk-OAE4IQX2.js} +769 -479
- package/dist/chunks/{chunk-IQELQNMR.js → chunk-U5SR2LZR.js} +17 -4
- package/dist/color-picker.js +2 -2
- package/dist/components/button/Button.d.ts +19 -2
- package/dist/components/card/Card.d.ts +1 -1
- package/dist/components/code-block/CodeBlock.d.ts +1 -1
- package/dist/components/color-picker/ColorPicker.d.ts +1 -1
- package/dist/components/data-table/Row.d.ts +3 -1
- package/dist/components/data-table/rows.d.ts +4 -0
- package/dist/components/digit-input/DigitInput.d.ts +6 -1
- package/dist/components/index.d.ts +4 -1
- package/dist/components/input/Input.d.ts +16 -1
- package/dist/components/input/passwordStrength.d.ts +9 -0
- package/dist/components/progress-bar/ProgressBar.d.ts +16 -1
- package/dist/components/sparkline/Sparkline.d.ts +26 -0
- package/dist/hooks/useEdgeOverflow.d.ts +4 -0
- package/dist/icons/registry.d.ts +2 -0
- package/dist/index.css +830 -267
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -3
- package/dist/internal/CheckMark.d.ts +12 -0
- package/dist/internal/MorphText.d.ts +12 -0
- package/dist/internal/RollingNumber.d.ts +14 -0
- package/dist/internal/surfaceDepth.d.ts +25 -0
- package/dist/internal/swatch.d.ts +4 -1
- package/dist/layout/app-shell/AppShell.d.ts +2 -2
- package/dist/motion/celebrate.d.ts +18 -0
- package/dist/tokens/layers.d.ts +37 -0
- package/dist/tokens/naming.d.ts +1 -1
- package/dist/tokens/semantic.d.ts +66 -56
- package/dist/tokens/themes/dark.d.ts +6 -14
- package/package.json +1 -1
- package/src/components/accordion/COMPONENT.md +4 -4
- package/src/components/avatar/COMPONENT.md +1 -1
- package/src/components/button/COMPONENT.md +26 -5
- package/src/components/button/examples/download.tsx +45 -0
- package/src/components/button/examples/hold-to-confirm.tsx +23 -0
- package/src/components/button/examples/label-morph.tsx +33 -0
- package/src/components/card/COMPONENT.md +4 -2
- package/src/components/card/examples/narrow.tsx +2 -2
- package/src/components/card/examples/panel-chart.tsx +1 -1
- package/src/components/card/examples/variants.tsx +1 -1
- package/src/components/checkbox/examples/examples.module.css +1 -1
- package/src/components/code-block/COMPONENT.md +6 -6
- package/src/components/code-block/examples/overview.tsx +1 -1
- package/src/components/code-block/examples/variants.tsx +1 -1
- package/src/components/color-picker/COMPONENT.md +1 -1
- package/src/components/color-swatches/COMPONENT.md +5 -5
- package/src/components/command-menu/COMPONENT.md +1 -1
- package/src/components/crossfade/examples/overview.tsx +3 -3
- package/src/components/data-table/COMPONENT.md +5 -4
- package/src/components/digit-input/COMPONENT.md +5 -3
- package/src/components/digit-input/examples/on-complete.tsx +7 -6
- package/src/components/dnd/COMPONENT.md +1 -1
- package/src/components/dnd/examples/examples.module.css +4 -4
- package/src/components/dropdown/COMPONENT.md +1 -1
- package/src/components/dropdown/examples/examples.module.css +1 -1
- package/src/components/empty-page/examples/examples.module.css +1 -1
- package/src/components/example-frame/COMPONENT.md +1 -1
- package/src/components/input/COMPONENT.md +10 -1
- package/src/components/input/examples/password-strength.tsx +12 -0
- package/src/components/kanban/COMPONENT.md +4 -4
- package/src/components/login-form/COMPONENT.md +4 -4
- package/src/components/login-form/examples/register.tsx +2 -2
- package/src/components/login-form/examples/reset-password.tsx +2 -2
- package/src/components/modal/COMPONENT.md +2 -2
- package/src/components/modal/examples/examples.module.css +2 -2
- package/src/components/page-content/examples/examples.module.css +1 -1
- package/src/components/popover/COMPONENT.md +1 -1
- package/src/components/progress-bar/COMPONENT.md +5 -1
- package/src/components/progress-bar/examples/examples.module.css +7 -0
- package/src/components/progress-bar/examples/milestone.tsx +43 -0
- package/src/components/progress-bar/examples/steps.tsx +35 -0
- package/src/components/skeleton/examples/with-crossfade.tsx +3 -3
- package/src/components/sparkline/COMPONENT.md +98 -0
- package/src/components/sparkline/examples/examples.module.css +13 -0
- package/src/components/sparkline/examples/overview.tsx +27 -0
- package/src/components/sparkline/examples/period.tsx +46 -0
- package/src/components/tabs/COMPONENT.md +15 -16
- package/src/components/tabs/examples/examples.module.css +14 -0
- package/src/components/tabs/examples/orientation.tsx +87 -33
- package/src/components/tooltip/COMPONENT.md +8 -6
- package/src/components/tooltip/examples/examples.module.css +2 -2
- package/src/layout/app-header/COMPONENT.md +1 -1
- package/src/layout/app-header/examples/examples.module.css +4 -4
- package/src/layout/app-shell/COMPONENT.md +8 -8
- package/src/layout/app-shell/examples/examples.module.css +3 -3
- package/src/layout/bottom-nav/COMPONENT.md +1 -1
- package/src/layout/bottom-nav/examples/examples.module.css +1 -1
- package/src/layout/sidebar/COMPONENT.md +4 -4
- package/src/layout/sidebar/examples/examples.module.css +5 -5
- package/src/styles/globals.css +125 -2
- package/src/styles/theme-dark.css +36 -13
- package/src/styles/theme-light.css +60 -16
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Sign-up: five fields in one column, a password mismatch shown as the field error — `error`, `LoginForm.Form`. */
|
|
1
|
+
/** Sign-up: five fields in one column, a strength meter on the new password and a mismatch shown as the field error — `strength`, `error`, `LoginForm.Form`. */
|
|
2
2
|
import { Button, Divider, Icon, Input, LinkButton, LoginForm } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
@@ -45,7 +45,7 @@ export default function LoginFormRegisterExample() {
|
|
|
45
45
|
<Input.Field placeholder="@login" />
|
|
46
46
|
</Input.Wrapper>
|
|
47
47
|
</Input.Root>
|
|
48
|
-
<Input.Root label="Пароль" required hint="Минимум
|
|
48
|
+
<Input.Root label="Пароль" required hint="Минимум 8 символов" strength>
|
|
49
49
|
<Input.Wrapper>
|
|
50
50
|
<Input.Field
|
|
51
51
|
type="password"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** A new password after the e-mail link:
|
|
1
|
+
/** A new password after the e-mail link: a strength meter on the first field and a mismatch error under the second one — `strength`, `error`. */
|
|
2
2
|
import { Button, Icon, Input, LoginForm } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
@@ -23,7 +23,7 @@ export default function LoginFormResetPasswordExample() {
|
|
|
23
23
|
</LoginForm.Header>
|
|
24
24
|
<LoginForm.Body>
|
|
25
25
|
<LoginForm.Form onSubmit={(e) => e.preventDefault()}>
|
|
26
|
-
<Input.Root label="Новый пароль" required hint="Минимум
|
|
26
|
+
<Input.Root label="Новый пароль" required hint="Минимум 8 символов" strength>
|
|
27
27
|
<Input.Wrapper>
|
|
28
28
|
<Input.Field
|
|
29
29
|
type="password"
|
|
@@ -107,7 +107,7 @@ No DOM: clone the single child and chain its `onClick` (unless the child's handl
|
|
|
107
107
|
| `children` | `ReactElement` | — (required) | One element, usually a Button. |
|
|
108
108
|
|
|
109
109
|
## Variants
|
|
110
|
-
The dialog has one look: `
|
|
110
|
+
The dialog has one look: the floating layer of the ladder (`data-depth="floating"`: white in light, a step above a card in dark), `--prime-modal-radius` (16), `shadow-modal`, on a `bg-scrim`. Header, body and footer are separated by faint inset hairlines.
|
|
111
111
|
|
|
112
112
|
### size (Content)
|
|
113
113
|
| Value | Looks like | Use when | Default |
|
|
@@ -165,7 +165,7 @@ Loading or disabled actions use the Button props (`loading`, `disabled`) inside
|
|
|
165
165
|
## Layout & spacing
|
|
166
166
|
- Dialog padding `--prime-modal-padding` (24) on every zone; header 24 top / 20 bottom, body 20 with a 16 gap between blocks, footer 20 top / 24 bottom.
|
|
167
167
|
- Max height = viewport minus `--prime-modal-viewport-padding` on both sides; only the body scrolls.
|
|
168
|
-
- Fields inside
|
|
168
|
+
- Fields inside take the floating layer's fill; a card inside is the next layer (`floating-1`), a flat tile. Group fields with a 20 gap.
|
|
169
169
|
- Narrow viewport (< 640px): bottom sheet, full width, top corners `--prime-sheet-radius`, a grab handle strip on top, up to `100dvh − --prime-sheet-top-gap` high, the bottom safe-area inset below the content.
|
|
170
170
|
|
|
171
171
|
## Accessibility
|
|
@@ -16,6 +16,6 @@
|
|
|
16
16
|
width: 100%;
|
|
17
17
|
padding: var(--prime-space-4);
|
|
18
18
|
border-radius: var(--prime-card-radius);
|
|
19
|
-
background: var(--prime-color-
|
|
20
|
-
box-shadow: var(--prime-
|
|
19
|
+
background: var(--prime-color-layer-nested);
|
|
20
|
+
box-shadow: var(--prime-shadow-raised);
|
|
21
21
|
}
|
|
@@ -77,7 +77,7 @@ No DOM: clone the single child and merge `ref` and `onClick`. Trigger toggles th
|
|
|
77
77
|
`ref` → the element. `<div>` (title + description with a 4 px step) / `<h2>` (names the dialog) / `<p>` (describes it) / `<div>` (buttons at the end, stacked full width on phones, below 640 px). + native props.
|
|
78
78
|
|
|
79
79
|
## Variants
|
|
80
|
-
The panel has one look: `
|
|
80
|
+
The panel has one look: the floating layer of the ladder (`data-depth="floating"`), `--prime-panel-radius`, `shadow-overlay`, no border. Fields inside take that layer's fill; a card inside is the next layer, a flat tile.
|
|
81
81
|
|
|
82
82
|
### size (Content)
|
|
83
83
|
| Value | Looks like | Use when | Default |
|
|
@@ -51,6 +51,7 @@ A native `<progress>` (transparent, for assistive tech) under the drawn line.
|
|
|
51
51
|
| `value` | `number` | — (required) | The current value, clamped to `0…max`. |
|
|
52
52
|
| `max` | `number` | `100` | Top of the scale. |
|
|
53
53
|
| `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"accent"` | Fill color, telling the outcome. |
|
|
54
|
+
| `steps` | `boolean` | `false` | Draws the scale as `max` equal cells (2–12), each filled whole; `value` rounds to a cell. In one change the cells go one after another — from the start when filling, from the end when emptying. For a level out of a few: password strength, onboarding steps. |
|
|
54
55
|
|
|
55
56
|
### ProgressBar · segments mode
|
|
56
57
|
A `role="group"` bar of parts, named by `label` and described by the distribution text.
|
|
@@ -100,8 +101,9 @@ A `role="group"` bar of parts, named by `label` and described by the distributio
|
|
|
100
101
|
| tone / size | props | `data-tone` (value mode), `data-size` on the root |
|
|
101
102
|
| value | `value`, `max` | native `value` / `max`; the drawn pill slides by `translate` over `slow` + `standard`, RTL aware |
|
|
102
103
|
| segments | `segments`, `max`, `segmentGap` | `data-segment-gap` on the group, `data-tone` per part; widths animate by `flex-grow` over `slow` |
|
|
104
|
+
| steps | `steps` (value mode) | `data-steps="true"` on the bar; `max` cells (2–12), filled ones `data-filled`; a cell fills from its start edge (`base` · emphasized), cells of one change go half a stagger step apart — from the start when filling, from the end when emptying |
|
|
103
105
|
| empty segments | `segments` empty or all `0` | only the track; accessible text `labels.empty` / `labels.allEmpty` |
|
|
104
|
-
| reduced motion | `prefers-reduced-motion` | fill and
|
|
106
|
+
| reduced motion | `prefers-reduced-motion` | fill, widths and cells change instantly |
|
|
105
107
|
|
|
106
108
|
Always controlled: pass the current `value` or `segments` on every render.
|
|
107
109
|
|
|
@@ -137,6 +139,8 @@ No keyboard interaction.
|
|
|
137
139
|
| [sizes.tsx](examples/sizes.tsx) | Every tier: the line grows from 4 to 8 px like the Slider track — `size`. |
|
|
138
140
|
| [custom-max.tsx](examples/custom-max.tsx) | A scale of steps instead of percent: 3 of 5 profile steps — `max`. |
|
|
139
141
|
| [segments.tsx](examples/segments.tsx) | Parts of a whole in one bar: joined or separate pills, free capacity up to `max` and an empty list — `segments`, `segmentGap`, `max`. |
|
|
142
|
+
| [steps.tsx](examples/steps.tsx) | The scale as whole cells that fill one after another: 2 of 5 onboarding steps, forward and back — `steps`, `max`. |
|
|
143
|
+
| [milestone.tsx](examples/milestone.tsx) | A rare milestone: the last invoice of the quarter fills the bar, the tone turns success and a short confetti burst marks it — `value`, `tone`, `celebrate()`. |
|
|
140
144
|
|
|
141
145
|
## Mistakes
|
|
142
146
|
- No `label` and no `aria-label` → the bar has no accessible name.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** A rare milestone: the last invoice of the quarter fills the bar, the tone turns success and a short confetti burst marks it — `value`, `tone`, `celebrate()`. */
|
|
2
|
+
import { Button, celebrate, ProgressBar } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./examples.module.css";
|
|
6
|
+
|
|
7
|
+
const INVOICES = 12;
|
|
8
|
+
|
|
9
|
+
export default function ProgressBarMilestoneExample() {
|
|
10
|
+
const [paid, setPaid] = React.useState(INVOICES - 1);
|
|
11
|
+
const closed = paid === INVOICES;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={styles.column}>
|
|
15
|
+
<ProgressBar
|
|
16
|
+
value={paid}
|
|
17
|
+
max={INVOICES}
|
|
18
|
+
tone={closed ? "success" : "accent"}
|
|
19
|
+
label={
|
|
20
|
+
closed
|
|
21
|
+
? "III квартал закрыт: все счета оплачены"
|
|
22
|
+
: `Оплачено счетов: ${paid} из ${INVOICES}`
|
|
23
|
+
}
|
|
24
|
+
/>
|
|
25
|
+
<div className={styles.actions}>
|
|
26
|
+
<Button.Root
|
|
27
|
+
variant={closed ? "soft" : "solid"}
|
|
28
|
+
tone={closed ? "neutral" : "accent"}
|
|
29
|
+
onClick={(event) => {
|
|
30
|
+
if (closed) {
|
|
31
|
+
setPaid(INVOICES - 1);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
setPaid(INVOICES);
|
|
35
|
+
celebrate({ origin: event.currentTarget });
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{closed ? "Вернуть последний счёт" : "Отметить оплату ООО «Вектор»"}
|
|
39
|
+
</Button.Root>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The scale as whole cells that fill one after another: 2 of 5 onboarding steps, forward and back — `steps`, `max`. */
|
|
2
|
+
import { Button, ProgressBar } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./examples.module.css";
|
|
6
|
+
|
|
7
|
+
const TOTAL = 5;
|
|
8
|
+
|
|
9
|
+
export default function ProgressBarStepsExample() {
|
|
10
|
+
const [done, setDone] = React.useState(2);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.column}>
|
|
14
|
+
<ProgressBar
|
|
15
|
+
steps
|
|
16
|
+
value={done}
|
|
17
|
+
max={TOTAL}
|
|
18
|
+
label={`Настройка компании: ${done} из ${TOTAL}`}
|
|
19
|
+
/>
|
|
20
|
+
<div className={styles.actions}>
|
|
21
|
+
<Button.Root
|
|
22
|
+
variant="soft"
|
|
23
|
+
tone="neutral"
|
|
24
|
+
disabled={done === 0}
|
|
25
|
+
onClick={() => setDone(done - 1)}
|
|
26
|
+
>
|
|
27
|
+
Назад
|
|
28
|
+
</Button.Root>
|
|
29
|
+
<Button.Root disabled={done === TOTAL} onClick={() => setDone(done + 1)}>
|
|
30
|
+
Следующий шаг
|
|
31
|
+
</Button.Root>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -5,9 +5,9 @@ import * as React from "react";
|
|
|
5
5
|
import styles from "./examples.module.css";
|
|
6
6
|
|
|
7
7
|
const PAYMENTS = [
|
|
8
|
-
{ id: "p1", payer: "ООО «Гранит»", amount: "
|
|
9
|
-
{ id: "p2", payer: "ИП Орлов Д. С.", amount: "
|
|
10
|
-
{ id: "p3", payer: "АО «Альфа Медиа»", amount: "
|
|
8
|
+
{ id: "p1", payer: "ООО «Гранит»", amount: "94 750 ₽" },
|
|
9
|
+
{ id: "p2", payer: "ИП Орлов Д. С.", amount: "18 000 ₽" },
|
|
10
|
+
{ id: "p3", payer: "АО «Альфа Медиа»", amount: "380 000 ₽" },
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
const LOAD_MS = 1500;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Sparkline
|
|
2
|
+
|
|
3
|
+
**Category:** data-display
|
|
4
|
+
**Kind:** primitive
|
|
5
|
+
|
|
6
|
+
> A small line chart with its headline — the latest value, its change against the point before and its date — where any point can be scrubbed.
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
- A metric card on a dashboard: revenue, orders, sign-ups over the last days or weeks.
|
|
10
|
+
- A trend next to a number where the shape matters more than exact axes.
|
|
11
|
+
- A detail page header that shows how a value moved.
|
|
12
|
+
|
|
13
|
+
## When not to use
|
|
14
|
+
- Exact values on axes, several series, or a legend → a full chart library; Sparkline has no axes.
|
|
15
|
+
- Progress toward a goal → [ProgressBar](../progress-bar/COMPONENT.md) or [ProgressCircle](../progress-circle/COMPONENT.md).
|
|
16
|
+
- A list of values to compare → [DataTable](../data-table/COMPONENT.md).
|
|
17
|
+
|
|
18
|
+
## Import
|
|
19
|
+
```tsx
|
|
20
|
+
import { Sparkline, type SparklinePoint } from "prime-ui-kit";
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Anatomy
|
|
24
|
+
```
|
|
25
|
+
Sparkline <div>; the headline and the chart
|
|
26
|
+
├─ head title (label) · date of the shown point
|
|
27
|
+
├─ value row value (rolling digits when settled) · change Badge with the trend arrow
|
|
28
|
+
└─ plot role="slider" over the svg line and area
|
|
29
|
+
└─ cursor guide line and dot of the shown point (aria-hidden)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
35
|
+
|
|
36
|
+
### Sparkline
|
|
37
|
+
`ref` → `HTMLDivElement` (the root). A headline (title · date, value · change) over a line chart; the chart is a `role="slider"` that picks a point.
|
|
38
|
+
|
|
39
|
+
| Prop | Type | Default | Description |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| `data` | `SparklinePoint[]` | — (required) | Points oldest first, `{ label, value }`; `label` names the point («8 окт»). Two or more draw a line. A new array cross-fades the line in. |
|
|
42
|
+
| `label` | `string` | — (required) | Visible title above the value and the accessible name of the chart. |
|
|
43
|
+
| `formatValue` | `(value: number) => string` | `Russian digit groups` | Display of a value in the headline and the spoken point («312 400 ₽»). |
|
|
44
|
+
| `labels` | `Partial<SparklineLabels>` | — | Built-in strings, see Labels. |
|
|
45
|
+
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className`, `data-*` and the other div attributes on the root. |
|
|
46
|
+
|
|
47
|
+
## Variants
|
|
48
|
+
No variants: one accent line on the surface it sits on. Put it in a `Card` for a dashboard tile.
|
|
49
|
+
|
|
50
|
+
## States
|
|
51
|
+
| State | Driven by | DOM |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| at rest | — | the latest point; the value rolls its digits when `data` changes |
|
|
54
|
+
| scrubbing | pointer or touch over the plot | `data-scrubbing` on the root; the cursor and the headline follow the pointer with no easing (direct manipulation), digits change in place |
|
|
55
|
+
| keyboard | arrows, Home, End on the focused plot | `aria-valuenow` / `aria-valuetext`; the cursor glides to the point (`base` · emphasized) |
|
|
56
|
+
| settle | pointer leaves, touch ends, focus leaves | the cursor glides back to the latest point |
|
|
57
|
+
| new series | a new `data` array | the line cross-fades in (swap motion); the trend arrow turns half a turn between up and down |
|
|
58
|
+
|
|
59
|
+
## Layout & spacing
|
|
60
|
+
- Takes the width of its container; the plot is 80 px high and inset by the dot radius so the cursor is never clipped.
|
|
61
|
+
- In a dashboard, wrap it in `Card.Root` + `Card.Body`; several cards go in a grid.
|
|
62
|
+
- Below 320 px the value row wraps the change badge under the value.
|
|
63
|
+
|
|
64
|
+
## Accessibility
|
|
65
|
+
|
|
66
|
+
### Keyboard
|
|
67
|
+
| Key | Action |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `←` · `→` | Previous or next point. |
|
|
70
|
+
| `Home` · `End` | First or last point. |
|
|
71
|
+
| `Tab` | Focuses the chart; leaving focus returns to the latest point. |
|
|
72
|
+
|
|
73
|
+
### ARIA
|
|
74
|
+
- The plot is `role="slider"` named by `label`; `aria-valuetext` speaks the point («8 окт: 312 400 ₽», `labels.point`).
|
|
75
|
+
- The headline with the latest value and change reads as plain text.
|
|
76
|
+
- The change carries a sign and an arrow, never color alone.
|
|
77
|
+
|
|
78
|
+
### Labels
|
|
79
|
+
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
80
|
+
|
|
81
|
+
| Key | Default | Used for |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `point` | `"{label}: {value}"` | Spoken value of the chosen point (`aria-valuetext`); `{label}` and `{value}` are replaced. |
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
| Example | Shows |
|
|
87
|
+
|---|---|
|
|
88
|
+
| [overview.tsx](examples/overview.tsx) | Revenue for 30 days in a dashboard card: drag across the chart, the headline follows and the trend arrow turns — `data`, `label`, `formatValue`. |
|
|
89
|
+
| [period.tsx](examples/period.tsx) | Another period swaps the series: the line cross-fades in and the headline digits roll to the new total — `data`. |
|
|
90
|
+
|
|
91
|
+
## Mistakes
|
|
92
|
+
- A new array on every render (`data={rows.map(...)}` inline) → the line cross-fades on every render; memoize the series.
|
|
93
|
+
- Formatting inside `label` of a point («312 400 ₽») → the point label is its name («8 окт»); format values with `formatValue`.
|
|
94
|
+
- A Sparkline without a container width (inside an inline flex row) → give it a width or put it in a `Card`.
|
|
95
|
+
|
|
96
|
+
## Related
|
|
97
|
+
- **Built from:** [Badge](../badge/COMPONENT.md)
|
|
98
|
+
- **See also:** [Card](../card/COMPONENT.md), [ProgressBar](../progress-bar/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* Layout of the Sparkline scenarios: semantic tokens only. */
|
|
2
|
+
|
|
3
|
+
/* A dashboard card's width: the chart takes the card width. */
|
|
4
|
+
.card {
|
|
5
|
+
width: min(100%, var(--prime-modal-width-s));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* The period switch above the chart inside the card body. */
|
|
9
|
+
.body {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: var(--prime-space-4);
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Revenue for 30 days in a dashboard card: drag across the chart, the headline follows and the trend arrow turns — `data`, `label`, `formatValue`. */
|
|
2
|
+
import { Card, Sparkline, type SparklinePoint } from "prime-ui-kit";
|
|
3
|
+
|
|
4
|
+
import styles from "./examples.module.css";
|
|
5
|
+
|
|
6
|
+
const MONTHS = ["янв", "фев", "мар", "апр", "мая", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
|
|
7
|
+
|
|
8
|
+
/** Daily revenue, 9 September — 8 October 2026. */
|
|
9
|
+
const REVENUE: SparklinePoint[] = Array.from({ length: 30 }, (_, i) => {
|
|
10
|
+
const day = new Date(2026, 8, 9 + i);
|
|
11
|
+
return {
|
|
12
|
+
label: `${day.getDate()} ${MONTHS[day.getMonth()]}`,
|
|
13
|
+
value: Math.round(260 + 40 * Math.sin(i / 3.2) + 28 * Math.sin(i / 1.4 + 1) + i * 2.6) * 1000,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const formatRubles = (value: number) => `${value.toLocaleString("ru-RU")} ₽`;
|
|
18
|
+
|
|
19
|
+
export default function SparklineOverviewExample() {
|
|
20
|
+
return (
|
|
21
|
+
<Card.Root className={styles.card}>
|
|
22
|
+
<Card.Body>
|
|
23
|
+
<Sparkline data={REVENUE} label="Выручка за 30 дней" formatValue={formatRubles} />
|
|
24
|
+
</Card.Body>
|
|
25
|
+
</Card.Root>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Another period swaps the series: the line cross-fades in and the headline digits roll to the new total — `data`. */
|
|
2
|
+
import { Card, SegmentedControl, Sparkline, type SparklinePoint } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./examples.module.css";
|
|
6
|
+
|
|
7
|
+
type Period = "week" | "month" | "quarter";
|
|
8
|
+
|
|
9
|
+
const series = (labels: string[], base: number, swing: number): SparklinePoint[] =>
|
|
10
|
+
labels.map((label, i) => ({
|
|
11
|
+
label,
|
|
12
|
+
value: Math.round((base + swing * Math.sin(i / 1.7) + (swing / 4) * i) / 100) * 100,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const SERIES: Record<Period, SparklinePoint[]> = {
|
|
16
|
+
week: series(["2 окт", "3 окт", "4 окт", "5 окт", "6 окт", "7 окт", "8 окт"], 41_000, 6_000),
|
|
17
|
+
month: series(
|
|
18
|
+
["сен, 1 нед.", "сен, 2 нед.", "сен, 3 нед.", "сен, 4 нед.", "окт, 1 нед."],
|
|
19
|
+
286_000,
|
|
20
|
+
32_000,
|
|
21
|
+
),
|
|
22
|
+
quarter: series(["авг", "сен", "окт"], 1_240_000, 160_000),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const formatRubles = (value: number) => `${value.toLocaleString("ru-RU")} ₽`;
|
|
26
|
+
|
|
27
|
+
export default function SparklinePeriodExample() {
|
|
28
|
+
const [period, setPeriod] = React.useState<Period>("month");
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Card.Root className={styles.card}>
|
|
32
|
+
<Card.Body className={styles.body}>
|
|
33
|
+
<SegmentedControl.Root
|
|
34
|
+
value={period}
|
|
35
|
+
onValueChange={(value) => setPeriod(value as Period)}
|
|
36
|
+
aria-label="Период"
|
|
37
|
+
>
|
|
38
|
+
<SegmentedControl.Item value="week">Неделя</SegmentedControl.Item>
|
|
39
|
+
<SegmentedControl.Item value="month">Месяц</SegmentedControl.Item>
|
|
40
|
+
<SegmentedControl.Item value="quarter">Квартал</SegmentedControl.Item>
|
|
41
|
+
</SegmentedControl.Root>
|
|
42
|
+
<Sparkline data={SERIES[period]} label="Новые заказы" formatValue={formatRubles} />
|
|
43
|
+
</Card.Body>
|
|
44
|
+
</Card.Root>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -26,8 +26,8 @@ import { Tabs } from "prime-ui-kit";
|
|
|
26
26
|
|
|
27
27
|
## Anatomy
|
|
28
28
|
```
|
|
29
|
-
Tabs.Root value, size, orientation, tone, fullWidth, min/maxItemWidth;
|
|
30
|
-
├─ Tabs.List role="tablist" on a ScrollContainer, sliding folder
|
|
29
|
+
Tabs.Root value, size, orientation, tone, fullWidth, min/maxItemWidth; the frame (a layer)
|
|
30
|
+
├─ Tabs.List role="tablist" on a ScrollContainer, sliding folder; collapses, then scrolls
|
|
31
31
|
│ ├─ Tabs.Item one tab (role="tab") in an item wrapper; onRemove adds a close button; parts:
|
|
32
32
|
│ │ ├─ Tabs.Icon decorative icon; hidden first, kept last
|
|
33
33
|
│ │ ├─ Tabs.Label title; keeps its width when it turns medium weight
|
|
@@ -42,14 +42,14 @@ Tabs.Root value, size, orientation, tone, fullWidth, min/maxItem
|
|
|
42
42
|
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
43
43
|
|
|
44
44
|
### Tabs.Root
|
|
45
|
-
`ref` → `HTMLDivElement`. `<div>` that owns the active value, size and orientation and lays out the list and the panel
|
|
45
|
+
`ref` → `HTMLDivElement`. `<div>` that owns the active value, size and orientation and lays out the list and the panel. It is the frame and a layer of the surface ladder in both orientations: a strip two steps off the layer and the panel in the layer's own color — above the panel when horizontal, at its inline start when vertical.
|
|
46
46
|
|
|
47
47
|
| Prop | Type | Default | Description |
|
|
48
48
|
|---|---|---|---|
|
|
49
49
|
| `value` | `string` | — | Active tab (controlled). |
|
|
50
50
|
| `defaultValue` | `string` | `""` | Initial active tab (uncontrolled). With `""` no tab is selected. |
|
|
51
51
|
| `onValueChange` | `(value: string) => void` | — | Called with the new active value. |
|
|
52
|
-
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | List direction and arrow keys.
|
|
52
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | List direction and arrow keys. Vertical: a column of sections at the panel's inline start, the active one the panel reaching into the column; when the container is narrower than 600px the column becomes the horizontal strip above the panel. |
|
|
53
53
|
| `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `"m"` | Control tier: text, icon, spacing, folder radius and panel padding. A horizontal tab is the control height plus the folder rise on both sides; a vertical one is the control height. |
|
|
54
54
|
| `fullWidth` | `boolean` | `true` | Horizontal tabs fill the list and tend to equal widths within `minItemWidth` … `maxItemWidth`, never narrower than their content: a long label keeps its width, short ones share the rest; when they do not fit, the list collapses, then scrolls. `false` sizes each tab to its content within the same bounds. |
|
|
55
55
|
| `tone` | `"neutral" \| "accent"` | `"neutral"` | Colour of the active tab: `neutral` is primary text with an accent icon, `accent` puts text and icon in accent. |
|
|
@@ -60,7 +60,7 @@ Tabs.Root value, size, orientation, tone, fullWidth, min/maxItem
|
|
|
60
60
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "defaultValue">` | — | `className` and the other div attributes. |
|
|
61
61
|
|
|
62
62
|
### Tabs.List
|
|
63
|
-
`ref` → `HTMLDivElement`. `role="tablist"` on a `ScrollContainer` (horizontal, edge fade, hidden scrollbar) with the sliding folder (
|
|
63
|
+
`ref` → `HTMLDivElement`. `role="tablist"` on a `ScrollContainer` (horizontal, edge fade, hidden scrollbar) with the sliding folder (in both orientations), which glides only into a choice by click or keys and is placed without motion on mount, resize and collapse. When tabs do not fit or a label would be cut, it hides icons and descriptions, then labels (icons with tooltips stay; only when every tab has an icon); then the list scrolls; a label is never cut to make room. The active tab is kept in view and clear of the edge fades (the first and the last tab scroll all the way to the end); only the list scrolls, never the page.
|
|
64
64
|
|
|
65
65
|
| Prop | Type | Default | Description |
|
|
66
66
|
|---|---|---|---|
|
|
@@ -128,7 +128,7 @@ Tabs.Root value, size, orientation, tone, fullWidth, min/maxItem
|
|
|
128
128
|
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className` and the other div attributes. |
|
|
129
129
|
|
|
130
130
|
## Variants
|
|
131
|
-
Tabs have no `variant`: navigation tabs are always a folder (horizontal) or
|
|
131
|
+
Tabs have no `variant`: navigation tabs are always a folder — rising from the panel's top edge (horizontal) or reaching out of its side (vertical).
|
|
132
132
|
|
|
133
133
|
### tone
|
|
134
134
|
| Value | Looks like | Use when | Default |
|
|
@@ -139,8 +139,8 @@ Tabs have no `variant`: navigation tabs are always a folder (horizontal) or pill
|
|
|
139
139
|
### orientation
|
|
140
140
|
| Value | Looks like | Use when | Default |
|
|
141
141
|
|---|---|---|---|
|
|
142
|
-
| `horizontal` | a `
|
|
143
|
-
| `vertical` |
|
|
142
|
+
| `horizontal` | a layer of the surface ladder: a `fill-muted-hover` strip (two steps off the layer) over a padded panel in the layer's own color, tabs edge to edge; the active tab is the panel rising into the strip with concave flares, gliding between tabs; primary text, medium weight, accent icon; hover is the same shape in `fill-faint` | switching sections above the content | yes |
|
|
143
|
+
| `vertical` | the same folder on its side: a column of sections in the `fill-muted-hover` strip at the panel's inline start; the active section is the panel reaching into the column, rounded on the outer side, with concave flares above and below where it meets the panel; hover is the same shape in `fill-faint`. Below a 600px container the column becomes the horizontal strip above the panel (it scrolls, never wraps) | settings pages with a side list of sections | |
|
|
144
144
|
|
|
145
145
|
### size
|
|
146
146
|
| Value | Looks like | Use when | Default |
|
|
@@ -180,26 +180,25 @@ Avoid icons or descriptions on only some tabs (uneven rows; without an icon on e
|
|
|
180
180
|
| State | Driven by | DOM |
|
|
181
181
|
|---|---|---|
|
|
182
182
|
| active | `value` / `defaultValue` | item `aria-selected="true"`, `data-state="active"`, `tabIndex=0`; others `data-state="inactive"`, `tabIndex=-1` |
|
|
183
|
-
| hover | pointer | text → primary;
|
|
183
|
+
| hover | pointer | text → primary; the folder shape in `fill-faint`, under the folder |
|
|
184
184
|
| pressed | pointer down | the tab content scales to `--prime-motion-press-scale-compact` |
|
|
185
|
-
| focus-visible | keyboard | focus ring inside the tab, above the folder; the panel shows an inset ring
|
|
185
|
+
| focus-visible | keyboard | focus ring inside the tab, above the folder; the panel shows an inset ring |
|
|
186
186
|
| disabled | `disabled` on Item | native `disabled`, `data-disabled="true"`, `text-disabled`; a removable tab loses its close button |
|
|
187
187
|
| removable | `onRemove` on Item | item wrapper `data-removable="true"`, close button, tab `aria-keyshortcuts="Delete"` |
|
|
188
188
|
| two-line | `Tabs.Description` child | `data-two-line="true"` |
|
|
189
189
|
| collapsed | tabs wider than the list, or a label that would be cut (horizontal) | list `data-collapse="full" \| "compact" \| "icon"`: `compact` hides icons and descriptions; `icon` hides labels (sr-only, shown in a `Tooltip`), counts and close buttons, only when every tab has an icon |
|
|
190
|
-
| overflow | tabs at their narrowest (content width, `minItemWidth` or icon squares) still wider than the list | `data-overflow-start` / `data-overflow-end` on the list (ScrollContainer), faded edges; the active tab is kept in view on mount and on every value change — only the list scrolls, never the page |
|
|
190
|
+
| overflow | tabs at their narrowest (content width, `minItemWidth` or icon squares) still wider than the list | `data-overflow-start` / `data-overflow-end` on the list (ScrollContainer), faded edges as deep as the hidden part at most (they grow and melt with the scroll, never switch); the active tab is kept in view and clear of the fades on mount and on every value change, the first and the last tab scroll all the way to the end — only the list scrolls, never the page |
|
|
191
191
|
|
|
192
192
|
The indicator glides (`emphasized` + `base`, `data-animate="true"`) only into a choice by click, arrow keys or closing the active tab; on mount, a value set from outside, a resize, font loading, tabs added or removed and collapse it is placed without motion, and it stays still under `prefers-reduced-motion`. The list scrolls smoothly to the active tab only after such a choice too.
|
|
193
193
|
|
|
194
|
-
Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-width`, inline `--tabs-item-min` / `--tabs-item-max` when set; List `data-indicator="folder" | "
|
|
194
|
+
Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-width`, inline `--tabs-item-min` / `--tabs-item-max` when set; List `data-indicator="folder"`, its indicator `data-axis="x" | "y"` (follows the list's direction, so a vertical list in a narrow container gets the top folder); Item `data-value` on the tab, `data-state` / `data-disabled` on its wrapper too; Label `data-text`. Inactive panels are unmounted; a panel's content enters each time it opens.
|
|
195
195
|
|
|
196
196
|
## Layout & spacing
|
|
197
|
-
-
|
|
198
|
-
- Vertical
|
|
197
|
+
- The root is the frame and a layer of the ladder in both orientations (`data-depth`, `radius-xl`, the strip two steps off the layer); the list sits flush on the panel; the panel pads its content by tier (`--prime-space-4` … `--prime-space-8`). Do not wrap Tabs in a Card — it already is one.
|
|
198
|
+
- Vertical: the column is as wide as its widest section (at least four control heights, at most 40% of the root), inset by the folder rise from the frame on the outer side, flush with the panel on the other.
|
|
199
199
|
- The list never wraps: it collapses, then scrolls horizontally with faded edges (hidden scrollbar, `overscroll-behavior: contain`). Give the list's container a width (`min-width: 0` on flex children).
|
|
200
200
|
- A horizontal tab is never narrower than its content (`min-width: max-content`; the tab bounds that content between `minItemWidth` and `maxItemWidth`), so a label is never cut to make room. Full width, tabs tend to equal widths, not widths in proportion to their labels (`flex: 1 1 0`): a long label keeps its width, the short ones share the rest.
|
|
201
201
|
- A vertical Tabs.Root is a size container: give it a width (it measures itself to switch to a row).
|
|
202
|
-
- In a vertical layout align the panel heading with the first tab by making the heading row one control tall (`min-height: var(--prime-control-m-height)`).
|
|
203
202
|
|
|
204
203
|
## Accessibility
|
|
205
204
|
|
|
@@ -235,7 +234,7 @@ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-
|
|
|
235
234
|
| [sizes.tsx](examples/sizes.tsx) | Every size tier: text, icon, folder radius and spacing grow together — `size`. |
|
|
236
235
|
| [states.tsx](examples/states.tsx) | A disabled tab that clicks and arrow keys skip — `disabled`. |
|
|
237
236
|
| [with-icon.tsx](examples/with-icon.tsx) | A muted icon before the label and a hairline before the service section — `Tabs.Icon`, `Tabs.Label`, `Tabs.Separator`. |
|
|
238
|
-
| [orientation.tsx](examples/orientation.tsx) |
|
|
237
|
+
| [orientation.tsx](examples/orientation.tsx) | Account settings with a side list of sections: the active section reaches out of the panel, and below 600px the list moves above it — `orientation`. |
|
|
239
238
|
| [overflow.tsx](examples/overflow.tsx) | Drag the frame narrower: icons go first, then labels, and icons with tooltips stay; then the list scrolls — `Tabs.Icon`. |
|
|
240
239
|
| [closable.tsx](examples/closable.tsx) | Open order cards as browser tabs: each closes by its button, Delete or a middle click; tabs stay between two widths, and the list scrolls when they do not fit — `onRemove`, `minItemWidth`, `maxItemWidth`. |
|
|
241
240
|
| [two-line.tsx](examples/two-line.tsx) | A label and a counter on the first line, a summary on the second — `Tabs.Count`, `Tabs.Description`. |
|
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
margin-inline: auto;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/* A settings block: wide enough for the side list next to a form, never wider than the preview. */
|
|
12
|
+
.wide {
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
width: min(100%, calc(20 * var(--prime-control-m-height)));
|
|
15
|
+
margin-inline: auto;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Fields and switches of a settings section: field → field rhythm (foundation §1). */
|
|
19
|
+
.form {
|
|
20
|
+
display: grid;
|
|
21
|
+
gap: var(--prime-space-5);
|
|
22
|
+
max-width: calc(10 * var(--prime-control-m-height));
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
/* The same column, which the reader can drag narrower to watch the tabs collapse and scroll. */
|
|
12
26
|
.resizable {
|
|
13
27
|
composes: frame;
|