prime-ui-kit 0.9.1 → 0.9.2
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 +22 -8
- package/dist/chunks/{chunk-KCQFOORX.js → chunk-IQELQNMR.js} +1 -1
- package/dist/chunks/{chunk-DB2Y3XXA.js → chunk-LXGRKN34.js} +2323 -1500
- package/dist/chunks/{chunk-NHOWNIAR.js → chunk-ZEFCDEEQ.js} +790 -536
- package/dist/color-picker.js +2 -2
- package/dist/components/data-table/DataTable.d.ts +6 -1
- package/dist/components/data-table/types.d.ts +5 -0
- package/dist/components/dnd/Dnd.d.ts +1 -1
- package/dist/components/dnd/dragSession.d.ts +9 -0
- package/dist/components/dnd/useDropTarget.d.ts +5 -0
- package/dist/components/dnd/useFlipList.d.ts +3 -3
- package/dist/components/drawer/Drawer.d.ts +8 -5
- package/dist/components/dropdown/Dropdown.d.ts +17 -0
- package/dist/components/index.d.ts +5 -1
- package/dist/components/kanban/Kanban.d.ts +129 -0
- package/dist/components/modal/Modal.d.ts +5 -2
- package/dist/components/page-toolbar/PageToolbar.d.ts +53 -0
- package/dist/components/segmented-control/SegmentedControl.d.ts +5 -2
- package/dist/components/tabs/Tabs.d.ts +5 -2
- package/dist/hooks/useEdgeOverflow.d.ts +12 -0
- package/dist/hooks/useMediaQuery.d.ts +7 -0
- package/dist/hooks/useSwipeDismiss.d.ts +34 -0
- package/dist/index.css +2271 -1163
- package/dist/index.js +10 -2
- package/dist/internal/overlay/FloatingPanel.d.ts +3 -1
- package/dist/internal/overlay/useFloatingLayer.d.ts +11 -1
- package/dist/internal/touchTarget.d.ts +4 -0
- package/dist/layout/app-header/AppHeader.d.ts +110 -0
- package/dist/layout/app-shell/AppShell.d.ts +14 -9
- package/dist/layout/bottom-nav/BottomNav.d.ts +80 -0
- package/dist/layout/index.d.ts +5 -1
- package/dist/tokens/primitives.d.ts +4 -0
- package/dist/tokens/semantic.d.ts +56 -0
- package/dist/tokens/themes/dark.d.ts +4 -0
- package/package.json +3 -2
- package/src/components/accordion/COMPONENT.md +1 -1
- package/src/components/badge/COMPONENT.md +2 -2
- package/src/components/banner/COMPONENT.md +1 -1
- package/src/components/checkbox/examples/examples.module.css +4 -2
- package/src/components/crossfade/COMPONENT.md +1 -1
- package/src/components/data-table/COMPONENT.md +19 -6
- package/src/components/data-table/examples/columns-visibility.tsx +167 -0
- package/src/components/digit-input/COMPONENT.md +10 -10
- package/src/components/digit-input/examples/examples.module.css +8 -0
- package/src/components/digit-input/examples/in-form.tsx +2 -5
- package/src/components/digit-input/examples/sizes.tsx +1 -1
- package/src/components/divider/COMPONENT.md +1 -1
- package/src/components/dnd/COMPONENT.md +3 -3
- package/src/components/drawer/COMPONENT.md +17 -9
- package/src/components/drawer/examples/date-sheet.tsx +41 -0
- package/src/components/drawer/examples/examples.module.css +5 -0
- package/src/components/drawer/examples/placement.tsx +4 -2
- package/src/components/dropdown/COMPONENT.md +19 -5
- package/src/components/dropdown/examples/checkbox-items.tsx +48 -0
- package/src/components/empty-page/COMPONENT.md +1 -1
- package/src/components/kanban/COMPONENT.md +181 -0
- package/src/components/kanban/examples/controlled.tsx +78 -0
- package/src/components/kanban/examples/examples.module.css +31 -0
- package/src/components/kanban/examples/limits.tsx +58 -0
- package/src/components/kanban/examples/narrow.tsx +76 -0
- package/src/components/kanban/examples/overview.tsx +130 -0
- package/src/components/kanban/examples/states.tsx +75 -0
- package/src/components/kanban/examples/structure.tsx +76 -0
- package/src/components/login-form/COMPONENT.md +1 -1
- package/src/components/modal/COMPONENT.md +9 -6
- package/src/components/notification/COMPONENT.md +2 -2
- package/src/components/page-content/COMPONENT.md +1 -1
- package/src/components/page-toolbar/COMPONENT.md +142 -0
- package/src/components/page-toolbar/examples/examples.module.css +16 -0
- package/src/components/page-toolbar/examples/narrow.tsx +103 -0
- package/src/components/page-toolbar/examples/overview.tsx +113 -0
- package/src/components/page-toolbar/examples/save-changes.tsx +40 -0
- package/src/components/page-toolbar/examples/without-search.tsx +60 -0
- package/src/components/popover/COMPONENT.md +4 -3
- package/src/components/progress-bar/COMPONENT.md +1 -1
- package/src/components/progress-circle/COMPONENT.md +1 -1
- package/src/components/scroll-container/COMPONENT.md +1 -1
- package/src/components/segmented-control/COMPONENT.md +8 -8
- package/src/components/segmented-control/examples/full-width.tsx +1 -1
- package/src/components/select/COMPONENT.md +2 -1
- package/src/components/skeleton/COMPONENT.md +1 -1
- package/src/components/smart-filter/COMPONENT.md +2 -0
- package/src/components/spinner/COMPONENT.md +1 -1
- package/src/components/tabs/COMPONENT.md +11 -8
- package/src/components/tabs/examples/closable.tsx +1 -1
- package/src/layout/app-header/COMPONENT.md +178 -0
- package/src/layout/app-header/examples/breadcrumbs.tsx +36 -0
- package/src/layout/app-header/examples/examples.module.css +47 -0
- package/src/layout/app-header/examples/in-app-shell.tsx +70 -0
- package/src/layout/app-header/examples/narrow.tsx +29 -0
- package/src/layout/app-header/examples/overview.tsx +36 -0
- package/src/layout/app-shell/COMPONENT.md +27 -21
- package/src/layout/app-shell/examples/examples.module.css +15 -0
- package/src/layout/app-shell/examples/footer.tsx +59 -0
- package/src/layout/app-shell/examples/overview.tsx +19 -8
- package/src/layout/bottom-nav/COMPONENT.md +153 -0
- package/src/layout/bottom-nav/examples/examples.module.css +39 -0
- package/src/layout/bottom-nav/examples/floating.tsx +89 -0
- package/src/layout/bottom-nav/examples/icon-only.tsx +41 -0
- package/src/layout/bottom-nav/examples/in-app-shell.tsx +64 -0
- package/src/layout/bottom-nav/examples/overview.tsx +44 -0
- package/src/layout/bottom-nav/examples/router.tsx +31 -0
- package/src/layout/bottom-nav/examples/structure.tsx +47 -0
- package/src/layout/sidebar/COMPONENT.md +11 -10
- package/src/styles/theme-dark.css +3 -0
- package/src/styles/theme-light.css +24 -0
- package/src/styles/tokens.css +2 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** The panel of an orders page: sections with counts, filter and search, the period and the primary action in one row; active filters in a row under it — `PageToolbar.Sections`, `PageToolbar.Tools`, `PageToolbar.View`, `PageToolbar.Actions`, `PageToolbar.Chips`. */
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Icon,
|
|
5
|
+
PageToolbar,
|
|
6
|
+
SegmentedControl,
|
|
7
|
+
Select,
|
|
8
|
+
SmartFilter,
|
|
9
|
+
type SmartFilterField,
|
|
10
|
+
type SmartFilterValue,
|
|
11
|
+
} from "prime-ui-kit";
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
|
|
14
|
+
const SECTIONS = [
|
|
15
|
+
{ value: "all", label: "Все", count: 128 },
|
|
16
|
+
{ value: "active", label: "В работе", count: 12 },
|
|
17
|
+
{ value: "done", label: "Доставлены", count: 96 },
|
|
18
|
+
{ value: "cancelled", label: "Отменены", count: 20 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const PERIODS = [
|
|
22
|
+
{ value: "7d", label: "7 дней" },
|
|
23
|
+
{ value: "30d", label: "30 дней" },
|
|
24
|
+
{ value: "quarter", label: "Квартал" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const FIELDS: SmartFilterField[] = [
|
|
28
|
+
{
|
|
29
|
+
key: "city",
|
|
30
|
+
label: "Город",
|
|
31
|
+
options: [
|
|
32
|
+
{ value: "msk", label: "Москва" },
|
|
33
|
+
{ value: "spb", label: "Санкт-Петербург" },
|
|
34
|
+
{ value: "kzn", label: "Казань" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "payment",
|
|
39
|
+
label: "Оплата",
|
|
40
|
+
options: [
|
|
41
|
+
{ value: "card", label: "Картой" },
|
|
42
|
+
{ value: "invoice", label: "По счёту" },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export default function PageToolbarOverviewExample() {
|
|
48
|
+
const [section, setSection] = React.useState("all");
|
|
49
|
+
const [period, setPeriod] = React.useState("30d");
|
|
50
|
+
const [filter, setFilter] = React.useState<SmartFilterValue>({
|
|
51
|
+
city: { include: ["msk"], exclude: [] },
|
|
52
|
+
});
|
|
53
|
+
const [search, setSearch] = React.useState("");
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<SmartFilter.Root
|
|
57
|
+
fields={FIELDS}
|
|
58
|
+
value={filter}
|
|
59
|
+
onValueChange={setFilter}
|
|
60
|
+
search={search}
|
|
61
|
+
onSearchChange={setSearch}
|
|
62
|
+
>
|
|
63
|
+
<PageToolbar.Root aria-label="Заказы">
|
|
64
|
+
<PageToolbar.Sections>
|
|
65
|
+
<SegmentedControl.Root
|
|
66
|
+
fullWidth
|
|
67
|
+
value={section}
|
|
68
|
+
onValueChange={setSection}
|
|
69
|
+
aria-label="Раздел"
|
|
70
|
+
>
|
|
71
|
+
{SECTIONS.map((item) => (
|
|
72
|
+
<SegmentedControl.Item key={item.value} value={item.value}>
|
|
73
|
+
{item.label}
|
|
74
|
+
<SegmentedControl.Count>{item.count}</SegmentedControl.Count>
|
|
75
|
+
</SegmentedControl.Item>
|
|
76
|
+
))}
|
|
77
|
+
</SegmentedControl.Root>
|
|
78
|
+
</PageToolbar.Sections>
|
|
79
|
+
<PageToolbar.Tools>
|
|
80
|
+
<SmartFilter.Toolbar />
|
|
81
|
+
</PageToolbar.Tools>
|
|
82
|
+
<PageToolbar.View>
|
|
83
|
+
<Select.Root value={period} onValueChange={setPeriod}>
|
|
84
|
+
<Select.Trigger aria-label="Период">
|
|
85
|
+
<Select.TriggerIcon>
|
|
86
|
+
<Icon name="field.calendar" />
|
|
87
|
+
</Select.TriggerIcon>
|
|
88
|
+
<Select.Value />
|
|
89
|
+
</Select.Trigger>
|
|
90
|
+
<Select.Content>
|
|
91
|
+
{PERIODS.map((item) => (
|
|
92
|
+
<Select.Item key={item.value} value={item.value}>
|
|
93
|
+
{item.label}
|
|
94
|
+
</Select.Item>
|
|
95
|
+
))}
|
|
96
|
+
</Select.Content>
|
|
97
|
+
</Select.Root>
|
|
98
|
+
</PageToolbar.View>
|
|
99
|
+
<PageToolbar.Actions>
|
|
100
|
+
<Button.Root>
|
|
101
|
+
<Button.Icon>
|
|
102
|
+
<Icon name="action.add" />
|
|
103
|
+
</Button.Icon>
|
|
104
|
+
Новый заказ
|
|
105
|
+
</Button.Root>
|
|
106
|
+
</PageToolbar.Actions>
|
|
107
|
+
<PageToolbar.Chips>
|
|
108
|
+
<SmartFilter.Chips />
|
|
109
|
+
</PageToolbar.Chips>
|
|
110
|
+
</PageToolbar.Root>
|
|
111
|
+
</SmartFilter.Root>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** A settings page: sections of the form on the left, the save action with the number of changes at the end of the top row — never a sticky bar at the bottom of a phone screen — `PageToolbar.Actions`. */
|
|
2
|
+
import { Button, PageToolbar, SegmentedControl } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
const SECTIONS = [
|
|
6
|
+
{ value: "profile", label: "Профиль" },
|
|
7
|
+
{ value: "billing", label: "Оплата" },
|
|
8
|
+
{ value: "team", label: "Команда" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const CHANGES = 3;
|
|
12
|
+
|
|
13
|
+
export default function PageToolbarSaveChangesExample() {
|
|
14
|
+
const [section, setSection] = React.useState("profile");
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<PageToolbar.Root aria-label="Настройки">
|
|
18
|
+
<PageToolbar.Sections>
|
|
19
|
+
<SegmentedControl.Root
|
|
20
|
+
fullWidth
|
|
21
|
+
value={section}
|
|
22
|
+
onValueChange={setSection}
|
|
23
|
+
aria-label="Раздел настроек"
|
|
24
|
+
>
|
|
25
|
+
{SECTIONS.map((item) => (
|
|
26
|
+
<SegmentedControl.Item key={item.value} value={item.value}>
|
|
27
|
+
{item.label}
|
|
28
|
+
</SegmentedControl.Item>
|
|
29
|
+
))}
|
|
30
|
+
</SegmentedControl.Root>
|
|
31
|
+
</PageToolbar.Sections>
|
|
32
|
+
<PageToolbar.Actions>
|
|
33
|
+
<Button.Root variant="ghost" tone="neutral">
|
|
34
|
+
Отменить
|
|
35
|
+
</Button.Root>
|
|
36
|
+
<Button.Root>Сохранить {CHANGES} изменения</Button.Root>
|
|
37
|
+
</PageToolbar.Actions>
|
|
38
|
+
</PageToolbar.Root>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** A dashboard panel without search on a tablet-width column: the sections fill the top row, the period selector alone in the bottom row stretches to the full width — `PageToolbar.Sections`, `PageToolbar.View`. */
|
|
2
|
+
import { Icon, PageToolbar, SegmentedControl, Select } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import styles from "./examples.module.css";
|
|
6
|
+
|
|
7
|
+
const SECTIONS = [
|
|
8
|
+
{ value: "overview", label: "Обзор" },
|
|
9
|
+
{ value: "activity", label: "Активность" },
|
|
10
|
+
{ value: "resources", label: "Ресурсы" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const PERIODS = [
|
|
14
|
+
{ value: "1h", label: "1 час" },
|
|
15
|
+
{ value: "24h", label: "24 часа" },
|
|
16
|
+
{ value: "7d", label: "7 дней" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export default function PageToolbarWithoutSearchExample() {
|
|
20
|
+
const [section, setSection] = React.useState("overview");
|
|
21
|
+
const [period, setPeriod] = React.useState("24h");
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={styles.tablet}>
|
|
25
|
+
<PageToolbar.Root aria-label="Панель мониторинга">
|
|
26
|
+
<PageToolbar.Sections>
|
|
27
|
+
<SegmentedControl.Root
|
|
28
|
+
fullWidth
|
|
29
|
+
value={section}
|
|
30
|
+
onValueChange={setSection}
|
|
31
|
+
aria-label="Раздел"
|
|
32
|
+
>
|
|
33
|
+
{SECTIONS.map((item) => (
|
|
34
|
+
<SegmentedControl.Item key={item.value} value={item.value}>
|
|
35
|
+
{item.label}
|
|
36
|
+
</SegmentedControl.Item>
|
|
37
|
+
))}
|
|
38
|
+
</SegmentedControl.Root>
|
|
39
|
+
</PageToolbar.Sections>
|
|
40
|
+
<PageToolbar.View>
|
|
41
|
+
<Select.Root value={period} onValueChange={setPeriod}>
|
|
42
|
+
<Select.Trigger aria-label="Период">
|
|
43
|
+
<Select.TriggerIcon>
|
|
44
|
+
<Icon name="field.calendar" />
|
|
45
|
+
</Select.TriggerIcon>
|
|
46
|
+
<Select.Value />
|
|
47
|
+
</Select.Trigger>
|
|
48
|
+
<Select.Content>
|
|
49
|
+
{PERIODS.map((item) => (
|
|
50
|
+
<Select.Item key={item.value} value={item.value}>
|
|
51
|
+
{item.label}
|
|
52
|
+
</Select.Item>
|
|
53
|
+
))}
|
|
54
|
+
</Select.Content>
|
|
55
|
+
</Select.Root>
|
|
56
|
+
</PageToolbar.View>
|
|
57
|
+
</PageToolbar.Root>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -61,7 +61,7 @@ No DOM: clone the single child and merge `ref` and `onClick`. Trigger toggles th
|
|
|
61
61
|
| `children` | `ReactElement` | — (required) | One element, usually a Button (Anchor: any element, e.g. a toolbar). |
|
|
62
62
|
|
|
63
63
|
### Popover.Content
|
|
64
|
-
`ref` → `HTMLDivElement`. Portal + `role="dialog"` on the floating surface (a ScrollContainer); renders while open and during its exit animation. Named by `Popover.Title`, else by the trigger. Opened from a Trigger, focus moves into the panel (`[data-autofocus]`, else the first tab stop, else the panel).
|
|
64
|
+
`ref` → `HTMLDivElement`. Portal + `role="dialog"` on the floating surface (a ScrollContainer); renders while open and during its exit animation. Named by `Popover.Title`, else by the trigger. Opened from a Trigger, focus moves into the panel (`[data-autofocus]`, else the first tab stop, else the panel). Below 640px of viewport a panel opened from a Trigger (not an Anchor) is a bottom sheet: a scrim, a grab handle, swipe down to close (with `closeOnOutsideClick`), page scroll locked.
|
|
65
65
|
|
|
66
66
|
| Prop | Type | Default | Description |
|
|
67
67
|
|---|---|---|---|
|
|
@@ -74,7 +74,7 @@ No DOM: clone the single child and merge `ref` and `onClick`. Trigger toggles th
|
|
|
74
74
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "role">` | — | `className` and the other attributes of the panel. |
|
|
75
75
|
|
|
76
76
|
### Popover.Header · Popover.Title · Popover.Description · Popover.Actions
|
|
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 below
|
|
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
80
|
The panel has one look: `bg-raised` fill, `--prime-panel-radius`, `shadow-overlay`, no border. Fields inside get the surface field fill; cards inside become sunken tiles.
|
|
@@ -121,6 +121,7 @@ Title uses the tier text size with title weight; Description uses the tier label
|
|
|
121
121
|
| size | `size` on Content | `data-size` |
|
|
122
122
|
| width | `matchTriggerWidth` | `data-match-trigger-width="true"` |
|
|
123
123
|
| flush | `flush` | `data-flush="true"` |
|
|
124
|
+
| sheet | viewport below 640px, opened from a Trigger (an Anchor keeps the panel next to its field) | a scrim and a full-width bottom sheet with an `aria-hidden` grab handle around the panel; the sheet carries `data-state`, the panel keeps its role and ref; page scroll locked; a swipe down from the handle closes it (with `closeOnOutsideClick`) |
|
|
124
125
|
|
|
125
126
|
There is no `disabled` on Popover: a disabled trigger never opens it.
|
|
126
127
|
|
|
@@ -128,7 +129,7 @@ There is no `disabled` on Popover: a disabled trigger never opens it.
|
|
|
128
129
|
- Width is `max-content` between `--prime-panel-min-width` and twice that, never wider than the viewport; give forms an explicit width with `className`.
|
|
129
130
|
- Max height follows the free space on the resolved side; the panel scrolls inside.
|
|
130
131
|
- Kept `--prime-space-2` from viewport edges; follows the trigger on scroll and resize.
|
|
131
|
-
- Actions: at the end, `--prime-space-2` gap; below
|
|
132
|
+
- Actions: at the end, `--prime-space-2` gap; on phones (below 640px) they stack full width.
|
|
132
133
|
|
|
133
134
|
## Accessibility
|
|
134
135
|
|
|
@@ -27,7 +27,7 @@ import { SegmentedControl } from "prime-ui-kit";
|
|
|
27
27
|
SegmentedControl.Root role="radiogroup"; track + sliding thumb, scrolling row, keyboard
|
|
28
28
|
└─ SegmentedControl.Item role="radio" button, one option
|
|
29
29
|
├─ SegmentedControl.Icon icon slot (optional; icon-only → square segment)
|
|
30
|
-
├─ SegmentedControl.Label title,
|
|
30
|
+
├─ SegmentedControl.Label title, one line (optional; plain text is wrapped automatically)
|
|
31
31
|
├─ SegmentedControl.Count counter Badge after the label (optional)
|
|
32
32
|
└─ SegmentedControl.Description second line; makes the segment two-line (optional)
|
|
33
33
|
```
|
|
@@ -46,12 +46,12 @@ SegmentedControl.Root role="radiogroup"; track + sliding thumb,
|
|
|
46
46
|
| `onValueChange` | `(value: string) => void` | — | Called with the new value on click or arrow keys. |
|
|
47
47
|
| `disabled` | `boolean` | `false` | Disables the whole group. |
|
|
48
48
|
| `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `host tier, else "m"` | Control tier; the outer height equals the control height 28 · 32 · 36 · 40 · 48. Without it the tier of its host (a toolbar, a panel with a size), else `m`. |
|
|
49
|
-
| `fullWidth` | `boolean` | `false` | Fills the container; single-line segments
|
|
49
|
+
| `fullWidth` | `boolean` | `false` | Fills the container; single-line segments tend to equal widths but never get narrower than their label — when they do not fit, the row scrolls. |
|
|
50
50
|
| `children` | `ReactNode` | — | `SegmentedControl.Item`s (may be wrapped, e.g. in `Tooltip.Trigger`). |
|
|
51
51
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "defaultValue" \| "onChange">` | — | `aria-label` / `aria-labelledby` (name the group), `className` and the other div attributes. |
|
|
52
52
|
|
|
53
53
|
### SegmentedControl.Item
|
|
54
|
-
`ref` → `HTMLButtonElement`. One option, a `<button role="radio">` with roving `tabIndex`; plain text is wrapped
|
|
54
|
+
`ref` → `HTMLButtonElement`. One option, a `<button role="radio">` with roving `tabIndex`; plain text is wrapped in a `SegmentedControl.Label`.
|
|
55
55
|
|
|
56
56
|
| Prop | Type | Default | Description |
|
|
57
57
|
|---|---|---|---|
|
|
@@ -70,7 +70,7 @@ SegmentedControl.Root role="radiogroup"; track + sliding thumb,
|
|
|
70
70
|
| `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other span attributes. |
|
|
71
71
|
|
|
72
72
|
### SegmentedControl.Label
|
|
73
|
-
`ref` → `HTMLSpanElement`. Segment title
|
|
73
|
+
`ref` → `HTMLSpanElement`. Segment title on one line; never truncated. Plain text is wrapped automatically.
|
|
74
74
|
|
|
75
75
|
| Prop | Type | Default | Description |
|
|
76
76
|
|---|---|---|---|
|
|
@@ -123,7 +123,7 @@ Track is `fill-muted` with a faint 1px ring; the selected segment is a floating
|
|
|
123
123
|
### Flags
|
|
124
124
|
| Value | Looks like | Use when | Default |
|
|
125
125
|
|---|---|---|---|
|
|
126
|
-
| `fullWidth` | fills the container; single-line segments
|
|
126
|
+
| `fullWidth` | fills the container; single-line segments tend to equal widths, a long label keeps its own width and the row scrolls when they do not fit; two-line segments grow from their content | modals, mobile panels, overview strips | off |
|
|
127
127
|
| `disabled` | every segment disabled, thumb `fill-muted-hover` without shadow | the choice is temporarily unavailable | off |
|
|
128
128
|
| icon-only (only `SegmentedControl.Icon`) | square segment (`data-icon-only`) | toolbar view toggles; needs `aria-label` | — |
|
|
129
129
|
|
|
@@ -140,13 +140,13 @@ Colour all items or none; do not give an item and its Count the same hue; prefer
|
|
|
140
140
|
| focus-visible | keyboard | focus ring inside the segment |
|
|
141
141
|
| overflow | row wider than the container | root `data-overflow-start` / `data-overflow-end`: edge fades in the track color |
|
|
142
142
|
|
|
143
|
-
Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `data-value`, `data-color`. The thumb glides (`emphasized` + `base`, `data-animate`) only into a user's choice (click, arrows); a value set from outside and layout changes snap it; it stays still under `prefers-reduced-motion`. It sits inside the segment track, which clips it, so moving the selection never changes the row's scroll area
|
|
143
|
+
Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `data-value`, `data-color`. The thumb glides (`emphasized` + `base`, `data-animate`) only into a user's choice (click, arrows); a value set from outside and layout changes snap it; it stays still under `prefers-reduced-motion`. It sits inside the segment track, which clips it, so moving the selection never changes the row's scroll area. When the row scrolls, the selected segment is kept in view on mount and on every value change: only the row scrolls, never the page, and the scroll glides only after a user's choice.
|
|
144
144
|
|
|
145
145
|
## Layout & spacing
|
|
146
146
|
- Width is content-based by default; segments never wrap. In a narrow container the row scrolls horizontally with fading edges.
|
|
147
147
|
- In a toolbar: gap `--prime-space-3` between controls; keep every control at one size.
|
|
148
148
|
- Caption under a group: gap `--prime-space-2`.
|
|
149
|
-
- `fullWidth` inside modals and mobile sheets.
|
|
149
|
+
- `fullWidth` inside modals and mobile sheets. Single-line segments are `flex: 1 1 0` with `min-width: max-content`: short labels share the spare width equally, a long label keeps its own width, nothing truncates; when the labels do not fit, the row scrolls.
|
|
150
150
|
|
|
151
151
|
## Accessibility
|
|
152
152
|
|
|
@@ -177,7 +177,7 @@ No `labels`.
|
|
|
177
177
|
| [states.tsx](examples/states.tsx) | No selection yet, a disabled segment and a disabled group next to the default — `disabled`. |
|
|
178
178
|
| [with-icon.tsx](examples/with-icon.tsx) | An icon before the label and square icon-only segments named for screen readers — `SegmentedControl.Icon`, `aria-label`. |
|
|
179
179
|
| [colors.tsx](examples/colors.tsx) | A status per option: a dot of its hue before the label and a tinted thumb when chosen — `color`. |
|
|
180
|
-
| [full-width.tsx](examples/full-width.tsx) | The group fills its column; segments
|
|
180
|
+
| [full-width.tsx](examples/full-width.tsx) | The group fills its column; segments tend to equal widths and a long label keeps its own — `fullWidth`. |
|
|
181
181
|
| [scroll.tsx](examples/scroll.tsx) | Segments never wrap: in a column narrower than the row it scrolls with edge fades and brings the chosen segment into view. |
|
|
182
182
|
| [two-line.tsx](examples/two-line.tsx) | A label and a counter on the first line, a metric on the second — `SegmentedControl.Label`, `SegmentedControl.Count`, `SegmentedControl.Description`. |
|
|
183
183
|
| [controlled.tsx](examples/controlled.tsx) | The parent owns the choice and updates other content from it — `value`, `onValueChange`. |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The group fills its column; segments
|
|
1
|
+
/** The group fills its column; segments tend to equal widths and a long label keeps its own — `fullWidth`. */
|
|
2
2
|
import { SegmentedControl } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
export default function SegmentedControlFullWidthExample() {
|
|
@@ -95,7 +95,7 @@ Select.Root field frame: label · control · hint/error; v
|
|
|
95
95
|
`ref` → `HTMLSpanElement`. An `aria-hidden` `<span>` with a leading glyph at the tier icon size: before the value / before the option label. + native `<span>` props.
|
|
96
96
|
|
|
97
97
|
### Select.Content
|
|
98
|
-
`ref` → `HTMLDivElement` (the panel). Portal + the floating list panel, mounted while open and during its exit animation (closed, the items render hidden in place so the trigger knows their labels), shown with the overlay motion. Holds the search row, the `role="listbox"` and the empty state.
|
|
98
|
+
`ref` → `HTMLDivElement` (the panel). Portal + the floating list panel, mounted while open and during its exit animation (closed, the items render hidden in place so the trigger knows their labels), shown with the overlay motion. Holds the search row, the `role="listbox"` and the empty state. Below 640px of viewport the panel is a bottom sheet: a scrim, a grab handle, swipe down to close, page scroll locked.
|
|
99
99
|
|
|
100
100
|
| Prop | Type | Default | Description |
|
|
101
101
|
|---|---|---|---|
|
|
@@ -163,6 +163,7 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
163
163
|
| disabled | `disabled` | native `disabled`, `data-disabled="true"` |
|
|
164
164
|
| loading | `loading` | `data-loading="true"`, `aria-busy`; status row in the list |
|
|
165
165
|
| searching | typing in the search | panel `data-searching="true"`; groups without matches and separators hide; the match in an option's title, description and plain text is a `<mark>` in `accent-text` (not in the trigger) |
|
|
166
|
+
| sheet | viewport below 640px | a scrim and a full-width bottom sheet with an `aria-hidden` grab handle around the panel; the sheet carries `data-state`, the panel keeps its role and ref; page scroll locked; a swipe down from the handle closes it |
|
|
166
167
|
| option highlighted / selected / disabled | keyboard and pointer / value / `disabled` | `data-highlighted`, `data-selected` + `aria-selected`, `data-disabled` + `aria-disabled`; highlight is a fill only |
|
|
167
168
|
|
|
168
169
|
## Layout & spacing
|
|
@@ -117,6 +117,8 @@ same height and type.
|
|
|
117
117
|
|
|
118
118
|
## Layout & spacing
|
|
119
119
|
- The panel is a flush Popover as wide as the toolbar; every section is a full-width row with its own padding (`--prime-space-3` × `--prime-space-4`, footer `--prime-space-2` × `--prime-space-4`), Dividers run edge to edge, the field label column is `--prime-space-24`.
|
|
120
|
+
- The toolbar is one row of controls (`--prime-space-2` gap): the filter button keeps its width, the search grows from 9rem (`--prime-space-24` × 1.5). When less than 9rem is left beside the button, the search moves under it at full width; every row stays one control high.
|
|
121
|
+
- Chips are Badges of the toolbar tier on the badge scale (`xs` → `s`), so a tag sits lower than the controls above it; «Добавить фильтр» is a tag of the same tier and «Сбросить все» a LinkButton with the tag's text size.
|
|
120
122
|
- Values and chips wrap with `--prime-space-2` gaps; «Сбросить все» is pushed to the end.
|
|
121
123
|
- The root is a flex column (`--prime-space-3`): Toolbar, then Chips.
|
|
122
124
|
|
|
@@ -51,16 +51,16 @@ Tabs.Root value, size, orientation, tone, fullWidth, min/maxItem
|
|
|
51
51
|
| `onValueChange` | `(value: string) => void` | — | Called with the new active value. |
|
|
52
52
|
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | List direction and arrow keys. A vertical list stacks above the panel when the container is narrower than 600px. |
|
|
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
|
-
| `fullWidth` | `boolean` | `true` | Horizontal tabs
|
|
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. |
|
|
56
|
-
| `minItemWidth` | `number \| string` | `2.5 × control height` | Narrowest a horizontal tab with a label gets, px or a CSS length;
|
|
56
|
+
| `minItemWidth` | `number \| string` | `2.5 × control height` | Narrowest a horizontal tab with a label gets, px or a CSS length; a tab is also never narrower than its content (up to `maxItemWidth`). When tabs do not fit, the list collapses, then scrolls. Icon-only tabs are square. |
|
|
57
57
|
| `maxItemWidth` | `number \| string` | `7 × control height` | Widest a horizontal tab gets, px or a CSS length (`"none"` lifts the cap); a longer label ends with an ellipsis. |
|
|
58
58
|
| `labels` | `Partial<TabsLabels>` | — | Built-in strings, see Labels. |
|
|
59
59
|
| `children` | `ReactNode` | — | `Tabs.List` and `Tabs.Panel`s. |
|
|
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 (vertical: pill). 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);
|
|
63
|
+
`ref` → `HTMLDivElement`. `role="tablist"` on a `ScrollContainer` (horizontal, edge fade, hidden scrollbar) with the sliding folder (vertical: pill), 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: only the list scrolls, never the page.
|
|
64
64
|
|
|
65
65
|
| Prop | Type | Default | Description |
|
|
66
66
|
|---|---|---|---|
|
|
@@ -156,13 +156,13 @@ A horizontal tab is the control height of the tier plus the rise above and below
|
|
|
156
156
|
### Flags
|
|
157
157
|
| Value | Looks like | Use when | Default |
|
|
158
158
|
|---|---|---|---|
|
|
159
|
-
| `fullWidth` | horizontal tabs
|
|
159
|
+
| `fullWidth` | horizontal tabs fill the list and tend to equal widths (`flex: 1 1 0`, up to `maxItemWidth`), content centred; a tab is never narrower than its content or `minItemWidth`, so a long label keeps its width and the short ones share the rest; `false` sizes each tab to its content, the strip still spans the frame | `false` for tabs of very different label lengths | `true` |
|
|
160
160
|
|
|
161
161
|
### minItemWidth · maxItemWidth
|
|
162
162
|
| Value | Looks like | Use when | Default |
|
|
163
163
|
|---|---|---|---|
|
|
164
164
|
| defaults | a horizontal tab is 2.5 to 7 control heights wide (90 … 252 at `m`); tabs never stretch into wide bars | most screens | yes |
|
|
165
|
-
| lengths (`128`, `"16rem"`) | browser tabs: grow up to `maxItemWidth` with the label cut by an ellipsis past it,
|
|
165
|
+
| lengths (`128`, `"16rem"`) | browser tabs: grow up to `maxItemWidth` with the label cut by an ellipsis past it, never narrower than `minItemWidth` or their title up to the cap; in a crowded list the list scrolls | open documents, records, chats — many tabs with long titles | |
|
|
166
166
|
| `maxItemWidth="none"` | tabs share the whole strip | a few sections that must fill a wide frame | |
|
|
167
167
|
|
|
168
168
|
### Item content
|
|
@@ -187,14 +187,17 @@ Avoid icons or descriptions on only some tabs (uneven rows; without an icon on e
|
|
|
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 `minItemWidth`
|
|
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 |
|
|
191
|
+
|
|
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.
|
|
191
193
|
|
|
192
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" | "pill"`; 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.
|
|
193
195
|
|
|
194
196
|
## Layout & spacing
|
|
195
197
|
- Horizontal: the root is the frame (`radius-xl`, `bg-sunken`), the list sits flush on the panel and runs edge to edge; the panel pads its content by tier (`--prime-space-4` … `--prime-space-8`). Do not wrap Tabs in a Card — it already is one.
|
|
196
198
|
- Vertical list → panel: `var(--prime-space-6)`.
|
|
197
|
-
- The list never wraps: it collapses, then scrolls horizontally with faded edges. Give the list's container a width (`min-width: 0` on flex children).
|
|
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
|
+
- 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.
|
|
198
201
|
- A vertical Tabs.Root is a size container: give it a width (it measures itself to switch to a row).
|
|
199
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)`).
|
|
200
203
|
|
|
@@ -234,7 +237,7 @@ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-
|
|
|
234
237
|
| [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`. |
|
|
235
238
|
| [orientation.tsx](examples/orientation.tsx) | Tabs over the panel and a side list of sections that stacks on top below 600px — `orientation`. |
|
|
236
239
|
| [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`. |
|
|
237
|
-
| [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
|
|
240
|
+
| [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`. |
|
|
238
241
|
| [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`. |
|
|
239
242
|
| [controlled.tsx](examples/controlled.tsx) | The active tab lives in parent state, e.g. synced with the URL — `value`, `onValueChange`. |
|
|
240
243
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Open order cards as browser tabs: each closes by its button, Delete or a middle click; tabs stay between two widths and
|
|
1
|
+
/** 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`. */
|
|
2
2
|
import { Button, Tabs, Typography } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|