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.
Files changed (107) hide show
  1. package/README.md +22 -8
  2. package/dist/chunks/{chunk-KCQFOORX.js → chunk-IQELQNMR.js} +1 -1
  3. package/dist/chunks/{chunk-DB2Y3XXA.js → chunk-LXGRKN34.js} +2323 -1500
  4. package/dist/chunks/{chunk-NHOWNIAR.js → chunk-ZEFCDEEQ.js} +790 -536
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/data-table/DataTable.d.ts +6 -1
  7. package/dist/components/data-table/types.d.ts +5 -0
  8. package/dist/components/dnd/Dnd.d.ts +1 -1
  9. package/dist/components/dnd/dragSession.d.ts +9 -0
  10. package/dist/components/dnd/useDropTarget.d.ts +5 -0
  11. package/dist/components/dnd/useFlipList.d.ts +3 -3
  12. package/dist/components/drawer/Drawer.d.ts +8 -5
  13. package/dist/components/dropdown/Dropdown.d.ts +17 -0
  14. package/dist/components/index.d.ts +5 -1
  15. package/dist/components/kanban/Kanban.d.ts +129 -0
  16. package/dist/components/modal/Modal.d.ts +5 -2
  17. package/dist/components/page-toolbar/PageToolbar.d.ts +53 -0
  18. package/dist/components/segmented-control/SegmentedControl.d.ts +5 -2
  19. package/dist/components/tabs/Tabs.d.ts +5 -2
  20. package/dist/hooks/useEdgeOverflow.d.ts +12 -0
  21. package/dist/hooks/useMediaQuery.d.ts +7 -0
  22. package/dist/hooks/useSwipeDismiss.d.ts +34 -0
  23. package/dist/index.css +2271 -1163
  24. package/dist/index.js +10 -2
  25. package/dist/internal/overlay/FloatingPanel.d.ts +3 -1
  26. package/dist/internal/overlay/useFloatingLayer.d.ts +11 -1
  27. package/dist/internal/touchTarget.d.ts +4 -0
  28. package/dist/layout/app-header/AppHeader.d.ts +110 -0
  29. package/dist/layout/app-shell/AppShell.d.ts +14 -9
  30. package/dist/layout/bottom-nav/BottomNav.d.ts +80 -0
  31. package/dist/layout/index.d.ts +5 -1
  32. package/dist/tokens/primitives.d.ts +4 -0
  33. package/dist/tokens/semantic.d.ts +56 -0
  34. package/dist/tokens/themes/dark.d.ts +4 -0
  35. package/package.json +3 -2
  36. package/src/components/accordion/COMPONENT.md +1 -1
  37. package/src/components/badge/COMPONENT.md +2 -2
  38. package/src/components/banner/COMPONENT.md +1 -1
  39. package/src/components/checkbox/examples/examples.module.css +4 -2
  40. package/src/components/crossfade/COMPONENT.md +1 -1
  41. package/src/components/data-table/COMPONENT.md +19 -6
  42. package/src/components/data-table/examples/columns-visibility.tsx +167 -0
  43. package/src/components/digit-input/COMPONENT.md +10 -10
  44. package/src/components/digit-input/examples/examples.module.css +8 -0
  45. package/src/components/digit-input/examples/in-form.tsx +2 -5
  46. package/src/components/digit-input/examples/sizes.tsx +1 -1
  47. package/src/components/divider/COMPONENT.md +1 -1
  48. package/src/components/dnd/COMPONENT.md +3 -3
  49. package/src/components/drawer/COMPONENT.md +17 -9
  50. package/src/components/drawer/examples/date-sheet.tsx +41 -0
  51. package/src/components/drawer/examples/examples.module.css +5 -0
  52. package/src/components/drawer/examples/placement.tsx +4 -2
  53. package/src/components/dropdown/COMPONENT.md +19 -5
  54. package/src/components/dropdown/examples/checkbox-items.tsx +48 -0
  55. package/src/components/empty-page/COMPONENT.md +1 -1
  56. package/src/components/kanban/COMPONENT.md +181 -0
  57. package/src/components/kanban/examples/controlled.tsx +78 -0
  58. package/src/components/kanban/examples/examples.module.css +31 -0
  59. package/src/components/kanban/examples/limits.tsx +58 -0
  60. package/src/components/kanban/examples/narrow.tsx +76 -0
  61. package/src/components/kanban/examples/overview.tsx +130 -0
  62. package/src/components/kanban/examples/states.tsx +75 -0
  63. package/src/components/kanban/examples/structure.tsx +76 -0
  64. package/src/components/login-form/COMPONENT.md +1 -1
  65. package/src/components/modal/COMPONENT.md +9 -6
  66. package/src/components/notification/COMPONENT.md +2 -2
  67. package/src/components/page-content/COMPONENT.md +1 -1
  68. package/src/components/page-toolbar/COMPONENT.md +142 -0
  69. package/src/components/page-toolbar/examples/examples.module.css +16 -0
  70. package/src/components/page-toolbar/examples/narrow.tsx +103 -0
  71. package/src/components/page-toolbar/examples/overview.tsx +113 -0
  72. package/src/components/page-toolbar/examples/save-changes.tsx +40 -0
  73. package/src/components/page-toolbar/examples/without-search.tsx +60 -0
  74. package/src/components/popover/COMPONENT.md +4 -3
  75. package/src/components/progress-bar/COMPONENT.md +1 -1
  76. package/src/components/progress-circle/COMPONENT.md +1 -1
  77. package/src/components/scroll-container/COMPONENT.md +1 -1
  78. package/src/components/segmented-control/COMPONENT.md +8 -8
  79. package/src/components/segmented-control/examples/full-width.tsx +1 -1
  80. package/src/components/select/COMPONENT.md +2 -1
  81. package/src/components/skeleton/COMPONENT.md +1 -1
  82. package/src/components/smart-filter/COMPONENT.md +2 -0
  83. package/src/components/spinner/COMPONENT.md +1 -1
  84. package/src/components/tabs/COMPONENT.md +11 -8
  85. package/src/components/tabs/examples/closable.tsx +1 -1
  86. package/src/layout/app-header/COMPONENT.md +178 -0
  87. package/src/layout/app-header/examples/breadcrumbs.tsx +36 -0
  88. package/src/layout/app-header/examples/examples.module.css +47 -0
  89. package/src/layout/app-header/examples/in-app-shell.tsx +70 -0
  90. package/src/layout/app-header/examples/narrow.tsx +29 -0
  91. package/src/layout/app-header/examples/overview.tsx +36 -0
  92. package/src/layout/app-shell/COMPONENT.md +27 -21
  93. package/src/layout/app-shell/examples/examples.module.css +15 -0
  94. package/src/layout/app-shell/examples/footer.tsx +59 -0
  95. package/src/layout/app-shell/examples/overview.tsx +19 -8
  96. package/src/layout/bottom-nav/COMPONENT.md +153 -0
  97. package/src/layout/bottom-nav/examples/examples.module.css +39 -0
  98. package/src/layout/bottom-nav/examples/floating.tsx +89 -0
  99. package/src/layout/bottom-nav/examples/icon-only.tsx +41 -0
  100. package/src/layout/bottom-nav/examples/in-app-shell.tsx +64 -0
  101. package/src/layout/bottom-nav/examples/overview.tsx +44 -0
  102. package/src/layout/bottom-nav/examples/router.tsx +31 -0
  103. package/src/layout/bottom-nav/examples/structure.tsx +47 -0
  104. package/src/layout/sidebar/COMPONENT.md +11 -10
  105. package/src/styles/theme-dark.css +3 -0
  106. package/src/styles/theme-light.css +24 -0
  107. 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 480 px). + native props.
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 480px they stack full width.
132
+ - Actions: at the end, `--prime-space-2` gap; on phones (below 640px) they stack full width.
132
133
 
133
134
  ## Accessibility
134
135
 
@@ -1,6 +1,6 @@
1
1
  # ProgressBar
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** primitive
5
5
 
6
6
  > Linear progress: one value on a native `<progress>`, or `segments` that split a whole (storage by type, task statuses), with a label, a percentage and status colors.
@@ -1,6 +1,6 @@
1
1
  # ProgressCircle
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** primitive
5
5
 
6
6
  > Circular progress — the ring version of ProgressBar: one value or `segments` that split a whole, with status colors and optional content in the center.
@@ -1,6 +1,6 @@
1
1
  # ScrollContainer
2
2
 
3
- **Category:** layout
3
+ **Category:** page
4
4
  **Kind:** layout
5
5
 
6
6
  > A scroll region with the kit's thin scrollbar that shrinks correctly inside flex and grid parents.
@@ -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, truncates (optional; plain text is wrapped automatically)
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 share the width equally and truncate. |
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 to truncate.
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; truncates with an ellipsis. Plain text is wrapped automatically.
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 are equal and truncate, two-line segments grow from their content | modals, mobile panels, overview strips | off |
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; when the row scrolls, the chosen segment is scrolled into view.
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 share the width equally and truncate long labels — `fullWidth`. |
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 share the width equally and truncate long labels — `fullWidth`. */
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
@@ -1,6 +1,6 @@
1
1
  # Skeleton
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** primitive
5
5
 
6
6
  > A placeholder in the shape of the content that is loading — text lines, a control, an avatar, a block — so the layout is in place before the data arrives.
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  # Spinner
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** primitive
5
5
 
6
6
  > An indeterminate loading indicator: a ring with a gap that turns while a request runs.
@@ -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 share the list width equally, within `minItemWidth` … `maxItemWidth`; `false` sizes each tab to its content within the same bounds. |
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; past it the list scrolls. Icon-only tabs are square. |
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); tabs then shrink to `minItemWidth` and the list scrolls.
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 share the list width equally (up to `maxItemWidth`), content centred; `false` sizes each tab to its content, the strip still spans the frame | `false` for tabs of very different label lengths | `true` |
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, shrink to `minItemWidth` in a crowded list, then the list scrolls | open documents, records, chats — many tabs with long titles | |
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` (or icon squares) still wider than the list | `data-overflow-start` / `data-overflow-end` on the list (ScrollContainer), faded edges; the active tab scrolls into view |
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 scroll past the narrower one — `onRemove`, `minItemWidth`, `maxItemWidth`. |
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 scroll past the narrower one — `onRemove`, `minItemWidth`, `maxItemWidth`. */
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