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
@@ -10,6 +10,7 @@
10
10
  - Data that needs sorting, paging, bulk selection or a filter toolbar.
11
11
  - Hierarchical rows (`getRowChildren`) or rows with a detail panel (`renderExpanded`).
12
12
  - Wide numeric reports with a sticky head and first column.
13
+ - Wide records on narrow screens: let people choose the columns (`hiddenColumns`) instead of dropping them.
13
14
 
14
15
  ## When not to use
15
16
  - One or two KPIs → use [Card](../card/COMPONENT.md).
@@ -28,7 +29,7 @@ Exported types: `DataTableColumn<Row>`, `DataTableProps<Row>`, `DataTableSortSta
28
29
  ```
29
30
  DataTable card-fill block, radius 12, clips its content
30
31
  ├── toolbar optional panel above the table (search, filters, bulk actions)
31
- ├── scroll viewport ScrollContainer, both axes
32
+ ├── scroll viewport ScrollContainer, both axes, in a frame that holds the edge shadows
32
33
  │ └── table
33
34
  │ ├── thead [select all] [toggle] head cells; sortable heads hold a button
34
35
  │ └── tbody rows · sub-rows · detail rows · skeleton / empty / error row
@@ -46,6 +47,7 @@ Generic over `Row`; `ref` → `HTMLDivElement`. A card-fill block with an option
46
47
  | Prop | Type | Default | Description |
47
48
  |---|---|---|---|
48
49
  | `columns` | `DataTableColumn<Row>[]` | — (required) | Column definitions, see `DataTableColumn<Row>`. |
50
+ | `hiddenColumns` | `string[]` | — | Ids of columns that are not rendered (head, body, skeleton and state rows); widths are measured again. A column with `hideable: false` stays. The list and the control that changes it (a column chooser in `toolbar`) belong to the consumer; sorting by a hidden column keeps the row order. |
49
51
  | `rows` | `Row[]` | — (required) | Data; sorted in memory when a sort is set. |
50
52
  | `getRowKey` | `(row: Row, index: number) => Key` | — | Stable row id (position by default); selection, expansion and the enter animation of new rows are keyed by it. Pass it whenever sorting, selection or expansion is used. |
51
53
  | `getRowLabel` | `(row: Row) => string` | — | Human row name for the checkbox and toggle names (`{label}` in `labels`). |
@@ -78,8 +80,8 @@ Generic over `Row`; `ref` → `HTMLDivElement`. A card-fill block with an option
78
80
  | `loadingMore` | `boolean` | `false` | A server batch is loading: footer status, `aria-busy`. |
79
81
  | `onLoadMore` | `() => void \| Promise<void>` | — | Called at the end when every loaded row is shown and `hasMore` is set. |
80
82
  | `scrollHeight` | `number \| string` | `360 with paging="infinite"` | Max height of the scroll viewport (number = px). |
81
- | `stickyHeader` | `boolean` | `false` | The head sticks while the body scrolls. |
82
- | `stickyFirstColumn` | `boolean` | `false` | The first column (with the selection and toggle columns) sticks while scrolling sideways. |
83
+ | `stickyHeader` | `boolean` | `false` | The head sticks while the body scrolls (with `scrollHeight`); on screens lower than 480px it scrolls with the rows. |
84
+ | `stickyFirstColumn` | `boolean` | `false` | The first visible column (with the selection and toggle columns) sticks while scrolling sideways; the start edge shadow falls from its end edge. |
83
85
  | `showHeader` | `boolean` | `true` | Render `<thead>`. |
84
86
  | `fullWidth` | `boolean` | `true` | The table spans its container; `false` sizes it by its content. |
85
87
  | `rowDividers` | `boolean` | `true` | Hairlines between rows. |
@@ -108,6 +110,7 @@ One column of `columns`: data, not a part.
108
110
  | `numeric` | `boolean` | — | Tabular figures, no wrapping, end alignment unless `align` is set. |
109
111
  | `truncate` | `boolean` | — | One line with an ellipsis; width from `maxWidth` (or `width`); string values get a `title`. |
110
112
  | `grow` | `boolean` | — | Takes the free width and wraps its text; the table then fills its container. With `minWidth` it never gets narrower: below that the table scrolls instead of cells overlapping. |
113
+ | `hideable` | `boolean` | `true` | The column can be hidden through `hiddenColumns`; `false` for the key column that names the row. |
111
114
  | `width · minWidth · maxWidth` | `string` | — | CSS sizes of the column (`"14rem"`). |
112
115
 
113
116
  ## Variants
@@ -149,7 +152,8 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
149
152
  | `striped` | every second row one opaque step off the fill | long numeric grids | `false` |
150
153
  | `highlightRowOnHover` | `fill-subtle` wash on the hovered row | interactive lists | `true` |
151
154
  | `highlightColumnOnHover` | `fill-subtle` wash on the hovered column | comparison tables | `false` |
152
- | `stickyHeader` · `stickyFirstColumn` | opaque sticky head / first column with a hairline edge | long / wide tables in a `scrollHeight` window | `false` |
155
+ | `stickyHeader` · `stickyFirstColumn` | opaque sticky head / first column with a hairline edge; on screens lower than 480px the head scrolls with the rows | long / wide tables in a `scrollHeight` window | `false` |
156
+ | column `hideable` | `false`: the column stays even when listed in `hiddenColumns` | the key column that names the row | `true` |
153
157
  | `showHeader` | head row shown | off only for self-explanatory key/value tables | `true` |
154
158
  | `fullWidth` | spans the container; `false` = content width | `false` for short lookup tables in wide layouts | `true` |
155
159
  | column `numeric` | tabular figures, end-aligned, no wrap | numbers, money | — |
@@ -166,15 +170,20 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
166
170
  | selected | `selected` / `defaultSelected` | `aria-selected` rows with `accent-soft` fill; polite `labels.selectedCount` |
167
171
  | expanded | `expanded` / `defaultExpanded` | `data-expanded` on the parent row (one step darker); toggle `aria-expanded`, chevron turns 90° |
168
172
  | new rows | rows mounted by an expand or new in `rows` (by `getRowKey`) | `data-animate`: cells drop in from above (`enterMotion`); detail panels open and close through `grid-template-rows` (`data-state` on the detail row; it unmounts after the close) |
173
+ | overflowing sideways | the table is wider than its viewport | `data-overflow-start` (scrolled from the start) and `data-overflow-end` (more to the end) on the root: a soft shadow over that edge, from the end edge of the sticky first column for the start; fades in and out over `fast` |
174
+ | hidden columns | `hiddenColumns` | the listed columns (except `hideable: false`) are not rendered in the head, rows, skeleton or state rows; widths are measured again |
169
175
  | state swap | the body moves between loading, rows, empty and error | the body fades in over `base` (opacity only, the same swap motion as [Crossfade](../crossfade/COMPONENT.md)); the head stays still; nothing moves on the first render |
170
176
 
171
- Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data-sticky-header`, `data-sticky-first-column`, `data-table-width` (`fill` · `auto` · `grow`), `data-highlight-row`, `data-highlight-column`, `data-striped`, `data-loading`, `data-selectable`, `data-expandable`, `data-dragging` (while drag-selecting). Sorting and paging swap rows instantly; everything is still under `prefers-reduced-motion`.
177
+ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data-sticky-header`, `data-sticky-first-column`, `data-table-width` (`fill` · `auto` · `grow`), `data-highlight-row`, `data-highlight-column`, `data-striped`, `data-loading`, `data-selectable`, `data-expandable`, `data-dragging` (while drag-selecting), `data-overflow-start` · `data-overflow-end` (edges that hide columns). Sorting and paging swap rows instantly; everything is still under `prefers-reduced-motion`.
172
178
 
173
179
  ## Layout & spacing
174
180
  - The root fills its container on `--prime-color-card-bg`, radius 12, and clips full-bleed head and rows; on a card or a modal it becomes a sunken block — never wrap it in a Card.
175
181
  - Toolbar padding `--prime-space-3` × cell padding, items `gap: --prime-space-2`; below 30rem of table width toolbar and footer stack.
176
182
  - The footer has a hairline on top: range on the left, Pagination on the right. The range shows only with more than one page or with infinite scroll.
177
- - Columns scroll sideways inside the table; pin the identifying column with `stickyFirstColumn`. Works from 320 px.
183
+ - Columns scroll sideways inside the table, never the page; pin the identifying column with `stickyFirstColumn`. Works from 320 px.
184
+ - While columns are hidden past an edge, that edge shows a `--prime-space-4` shadow (the theme scrim at 40%) over the content; it takes no space and follows RTL. With `stickyFirstColumn` the start shadow falls from the pinned column's end edge.
185
+ - Fewer columns, less scrolling: put a column chooser in `toolbar` (a Dropdown with a `Dropdown.CheckboxItem` per column, the key column `hideable: false` shown checked and disabled) and pass `hiddenColumns`. Never hide columns silently by width.
186
+ - The sticky head stops sticking on screens lower than 480px (landscape phones), so it never takes a large share of the height.
178
187
  - Cell content: text, two-line text (title + `caption`), an Avatar with text, a Badge, or a control one tier down (`s` in an `m` table). Toolbar controls use `s` in an `m` table.
179
188
  - Column widths: `width` / `minWidth` / `maxWidth` strings; never size cells with custom CSS.
180
189
  - Sub-rows indent by `--dt-indent` per level (avatar of the tier + gap); override it on the root for other leading content.
@@ -225,6 +234,7 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
225
234
  | [nested-rows.tsx](examples/nested-rows.tsx) | Partners with their expense lines: sub-rows indented under the parent's name, a chevron toggle, together with selection and sorting — `getRowChildren`, `expanded`, `onExpandedChange`. |
226
235
  | [detail-panel.tsx](examples/detail-panel.tsx) | Order details in a full-width row under an expanded order, aligned with the first content column — `renderExpanded`, `defaultExpanded`. |
227
236
  | [sticky.tsx](examples/sticky.tsx) | Monthly sales by region in a 280 px window: the head and the region column stay while scrolling both ways — `stickyHeader`, `stickyFirstColumn`, `scrollHeight`. |
237
+ | [columns-visibility.tsx](examples/columns-visibility.tsx) | Orders with a column chooser in the toolbar: fewer columns, less sideways scrolling; the order number always stays — `hiddenColumns`, `hideable`, `toolbar`. |
228
238
  | [infinite-scroll.tsx](examples/infinite-scroll.tsx) | An audit log that reveals loaded rows in batches and then asks the server for more — `paging`, `infiniteBatchSize`, `hasMore`, `loadingMore`, `onLoadMore`. |
229
239
  | [states.tsx](examples/states.tsx) | Loading skeleton, an empty period and a load error with a retry: the head stays, only the body changes — `loading`, `loadingRows`, `empty`, `error`. |
230
240
  | [controlled.tsx](examples/controlled.tsx) | Sort and page owned by the parent (a URL or a store): a header click goes asc → desc → none, a new sort returns to page 1 — `sort`, `onSortChange`, `page`, `onPageChange`. |
@@ -239,6 +249,9 @@ Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data
239
249
  - A custom checkbox column → use `selectable` (range, drag and announcements built in).
240
250
  - A long table in a `scrollHeight` window without `stickyHeader` → the head scrolls away.
241
251
  - `paging="none"` for hundreds of rows → use `pages` or `infinite`.
252
+ - Dropping columns below a breakpoint → people lose data without knowing it; offer a column chooser with `hiddenColumns`.
253
+ - Letting the key column be hidden → set `hideable: false` on it, so every row keeps its name.
254
+ - Wrapping the table in an `overflow-x` box of your own → it already scrolls inside and shows the edge shadows.
242
255
 
243
256
  ## Related
244
257
  - **Built from:** [Checkbox](../checkbox/COMPONENT.md), [Button](../button/COMPONENT.md), [Pagination](../pagination/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md) (loading rows, in the table tier)
@@ -0,0 +1,167 @@
1
+ /** Orders with a column chooser in the toolbar: fewer columns, less sideways scrolling; the order number always stays — `hiddenColumns`, `hideable`, `toolbar`. */
2
+ import {
3
+ Badge,
4
+ Button,
5
+ DataTable,
6
+ type DataTableColumn,
7
+ Dropdown,
8
+ Icon,
9
+ Typography,
10
+ } from "prime-ui-kit";
11
+ import * as React from "react";
12
+
13
+ import styles from "./examples.module.css";
14
+
15
+ type Status = "paid" | "shipped" | "waiting";
16
+ type Order = {
17
+ id: string;
18
+ client: string;
19
+ manager: string;
20
+ city: string;
21
+ status: Status;
22
+ date: string;
23
+ items: number;
24
+ total: number;
25
+ };
26
+
27
+ const STATUS: Record<Status, { label: string; color: "green" | "blue" | "orange" }> = {
28
+ paid: { label: "Оплачен", color: "green" },
29
+ shipped: { label: "Отгружен", color: "blue" },
30
+ waiting: { label: "Ждёт оплаты", color: "orange" },
31
+ };
32
+
33
+ const ORDERS: Order[] = [
34
+ {
35
+ id: "№ 1040",
36
+ client: "ООО «Северный ветер»",
37
+ manager: "Ольга Смирнова",
38
+ city: "Москва",
39
+ status: "paid",
40
+ date: "02.10.2026",
41
+ items: 12,
42
+ total: 184_500,
43
+ },
44
+ {
45
+ id: "№ 1041",
46
+ client: "ИП Козлов",
47
+ manager: "Игорь Волков",
48
+ city: "Казань",
49
+ status: "waiting",
50
+ date: "03.10.2026",
51
+ items: 3,
52
+ total: 27_900,
53
+ },
54
+ {
55
+ id: "№ 1042",
56
+ client: "АО «Технопарк»",
57
+ manager: "Ольга Смирнова",
58
+ city: "Санкт-Петербург",
59
+ status: "shipped",
60
+ date: "04.10.2026",
61
+ items: 40,
62
+ total: 612_000,
63
+ },
64
+ {
65
+ id: "№ 1043",
66
+ client: "ООО «Лето»",
67
+ manager: "Мария Орлова",
68
+ city: "Екатеринбург",
69
+ status: "paid",
70
+ date: "05.10.2026",
71
+ items: 7,
72
+ total: 58_350,
73
+ },
74
+ {
75
+ id: "№ 1044",
76
+ client: "ООО «Гранит»",
77
+ manager: "Игорь Волков",
78
+ city: "Новосибирск",
79
+ status: "shipped",
80
+ date: "06.10.2026",
81
+ items: 18,
82
+ total: 241_800,
83
+ },
84
+ ];
85
+
86
+ const RUB = new Intl.NumberFormat("ru-RU", {
87
+ style: "currency",
88
+ currency: "RUB",
89
+ maximumFractionDigits: 0,
90
+ });
91
+
92
+ const COLUMNS: DataTableColumn<Order>[] = [
93
+ { id: "id", header: "Заказ", accessor: "id", hideable: false },
94
+ { id: "client", header: "Клиент", accessor: "client" },
95
+ { id: "manager", header: "Менеджер", accessor: "manager" },
96
+ { id: "city", header: "Город", accessor: "city" },
97
+ {
98
+ id: "status",
99
+ header: "Статус",
100
+ cell: (row) => (
101
+ <Badge.Root color={STATUS[row.status].color}>{STATUS[row.status].label}</Badge.Root>
102
+ ),
103
+ },
104
+ { id: "date", header: "Дата", accessor: "date", numeric: true },
105
+ { id: "items", header: "Позиций", accessor: "items", numeric: true },
106
+ {
107
+ id: "total",
108
+ header: "Сумма",
109
+ accessor: "total",
110
+ sortable: true,
111
+ numeric: true,
112
+ cell: (row) => RUB.format(row.total),
113
+ },
114
+ ];
115
+
116
+ export default function DataTableColumnsVisibilityExample() {
117
+ const [hidden, setHidden] = React.useState<string[]>(["city", "items"]);
118
+ const shown = COLUMNS.length - hidden.length;
119
+
120
+ const toggle = (id: string, visible: boolean) =>
121
+ setHidden((current) => (visible ? current.filter((c) => c !== id) : [...current, id]));
122
+
123
+ return (
124
+ <DataTable
125
+ columns={COLUMNS}
126
+ hiddenColumns={hidden}
127
+ rows={ORDERS}
128
+ getRowKey={(row) => row.id}
129
+ stickyFirstColumn
130
+ toolbar={
131
+ <div className={styles.toolbar}>
132
+ <Typography as="span" variant="body-s" tone="secondary">
133
+ Заказы за неделю: {ORDERS.length}
134
+ </Typography>
135
+ <Dropdown.Root>
136
+ <Dropdown.Trigger>
137
+ <Button.Root variant="outline" tone="neutral" size="s">
138
+ <Button.Icon>
139
+ <Icon name="view.preview" />
140
+ </Button.Icon>
141
+ Колонки: {shown} из {COLUMNS.length}
142
+ </Button.Root>
143
+ </Dropdown.Trigger>
144
+ <Dropdown.Content align="end" size="s">
145
+ <Dropdown.Group label="Колонки таблицы">
146
+ {COLUMNS.map((column) => (
147
+ <Dropdown.CheckboxItem
148
+ key={column.id}
149
+ checked={!hidden.includes(column.id)}
150
+ disabled={column.hideable === false}
151
+ onCheckedChange={(visible) => toggle(column.id, visible)}
152
+ >
153
+ {column.header}
154
+ </Dropdown.CheckboxItem>
155
+ ))}
156
+ </Dropdown.Group>
157
+ <Dropdown.Separator />
158
+ <Dropdown.Item disabled={hidden.length === 0} onSelect={() => setHidden([])}>
159
+ Показать все
160
+ </Dropdown.Item>
161
+ </Dropdown.Content>
162
+ </Dropdown.Root>
163
+ </div>
164
+ }
165
+ />
166
+ );
167
+ }
@@ -37,7 +37,7 @@ DigitInput field frame: label row → cells → support row (hint | e
37
37
  | Prop | Type | Default | Description |
38
38
  |---|---|---|---|
39
39
  | `length` | `number` | `4` | Number of cells. |
40
- | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `host tier, else "m"` | Tier: the cell is a square with the side of the control height (28 · 32 · 36 · 40 · 48); label and hint follow it. Without it the tier of its host (LoginForm, a panel), else `m`. |
40
+ | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `host tier, else "m"` | Tier: the cell is a square on its own scale, two steps above the control heights (36 · 40 · 48 · 56 · 64); label and hint follow the field tier. Without it the tier of its host (LoginForm, a panel), else `m`. |
41
41
  | `label` | `ReactNode` | — | Label above the cells; names the group (`aria-labelledby`) and focuses the first cell on click. Without it the group is named by `labels.group`. |
42
42
  | `required` | `boolean` | — | Red `*` after the label and native `required` on every cell. |
43
43
  | `optional` | `boolean` | — | Muted marker right after the label text (`labels.optional`). |
@@ -49,7 +49,7 @@ DigitInput field frame: label row → cells → support row (hint | e
49
49
  | `defaultValue` | `string` | `""` | Initial code when uncontrolled. |
50
50
  | `onValueChange` | `(value: string) => void` | — | Called with the joined digits on every change. |
51
51
  | `onComplete` | `(value: string) => void` | — | Called once when the last empty cell is filled (typing, paste or autofill). |
52
- | `fullWidth` | `boolean` | `false` | Cells share the container width and keep the tier height; otherwise square cells and the field hugs them. |
52
+ | `fullWidth` | `boolean` | `false` | Cells share the container width up to 1.5 × their side (the row centres beyond that) and keep the tier height; otherwise square cells and the field hugs them. |
53
53
  | `groupSize` | `number` | — | Splits the cells into groups of this size with a wider gap (`3` → 123 456). |
54
54
  | `mask` | `boolean` | `false` | Hides the digits (PIN): cells are `type="password"`. |
55
55
  | `name` | `string` | — | Name of a hidden input that carries the joined code in a native form submit. |
@@ -66,18 +66,18 @@ No `variant` or `tone`.
66
66
  ### size
67
67
  | Value | Looks like | Use when | Default |
68
68
  |---|---|---|---|
69
- | `xs` | 28 × 28 cells, radius 6, digits 12/16, gap 4 | Inline confirmation inside dense tables or popovers | |
70
- | `s` | 32 × 32 cells, radius 8, digits 14/20, gap 8 | Compact dialogs | |
71
- | `m` | 36 × 36 cells, radius 8, digits 16/24, gap 8 | Regular forms and modals | yes |
72
- | `l` | 40 × 40 cells, radius 10, digits 18/24, gap 8 | Standalone verification screens | |
73
- | `xl` | 48 × 48 cells, radius 12, digits 20/28, gap 12 | Full-page sign-in, mobile-first screens | |
69
+ | `xs` | 36 × 36 cells, radius 8, digits 16/24, gap 8 | A code inside a popover or a dense panel | |
70
+ | `s` | 40 × 40 cells, radius 10, digits 18/24, gap 8 | Compact dialogs | |
71
+ | `m` | 48 × 48 cells, radius 12, digits 20/28, gap 12 | Regular forms, modals, LoginForm | yes |
72
+ | `l` | 56 × 56 cells, radius 12, digits 24/32, gap 12 | Standalone verification screens | |
73
+ | `xl` | 64 × 64 cells, radius 16, digits 30/36, gap 16 | Full-page sign-in, mobile-first screens | |
74
74
 
75
- **Sizes:** digits are semibold and tabular, one type step larger than the tier text (`xs` uses the tier text). A cell of size T is as high as Input and Button of size T; the label and the hint take the same tier.
75
+ **Sizes:** cells have their own scale, two steps above the control heights: a code is the one thing on its screen and a small cell is hard to hit and to read (cell `xs` = control `m`, `s` = control `l`, `m` = control `xl`). Digits are semibold and tabular and grow with the cell; the label and the hint keep the field tier.
76
76
 
77
77
  ### Flags
78
78
  | Value | Looks like | Use when | Default |
79
79
  |---|---|---|---|
80
- | `fullWidth` | one row, cells share the width, tier height | cards, forms and narrow phone columns where a full-width button follows | `false` |
80
+ | `fullWidth` | one row, cells share the width up to 1.5 × their side, then the row centres; tier height | narrow phone columns where a full-width button follows | `false` |
81
81
  | `groupSize` | an extra gap before every group (`3` → 123 456) | codes of six digits and longer | — |
82
82
  | `mask` | dots instead of digits | PIN codes | `false` |
83
83
  | `required` / `optional` | red `*` / muted «необязательно» after the label | mark the minority of the form | — |
@@ -137,7 +137,7 @@ No `variant` or `tone`.
137
137
  | Example | Shows |
138
138
  |---|---|
139
139
  | [overview.tsx](examples/overview.tsx) | A six-digit code from SMS with its label and a hint — `label`, `hint`, `length`. |
140
- | [sizes.tsx](examples/sizes.tsx) | Every size; the cell is a square with the side of the control height — `size`. |
140
+ | [sizes.tsx](examples/sizes.tsx) | Every size; the cell is a square on its own scale, two steps above the control heights — `size`. |
141
141
  | [states.tsx](examples/states.tsx) | A default code next to a disabled one — `disabled`. |
142
142
  | [validation.tsx](examples/validation.tsx) | Required and optional markers, a hint and an error that replaces it — `required`, `optional`, `hint`, `error`. |
143
143
  | [grouped.tsx](examples/grouped.tsx) | A long code read in chunks with a wider gap between groups — `groupSize`. |
@@ -8,6 +8,14 @@
8
8
  width: min(100%, var(--prime-modal-width-s));
9
9
  }
10
10
 
11
+ /* in-form.tsx: the form hugs the cells; as a grid item the button stretches to the code's width. */
12
+ .form {
13
+ display: grid;
14
+ gap: var(--prime-space-6);
15
+ width: fit-content;
16
+ max-width: 100%;
17
+ }
18
+
11
19
  /* controlled.tsx: the code and its action on one line, bottoms aligned. */
12
20
  .row {
13
21
  display: flex;
@@ -16,20 +16,17 @@ export default function DigitInputInFormExample() {
16
16
  };
17
17
 
18
18
  return (
19
- <form className={styles.panel} noValidate onSubmit={submit}>
19
+ <form className={styles.form} noValidate onSubmit={submit}>
20
20
  <DigitInput
21
21
  label="PIN-код карты"
22
22
  name="pin"
23
23
  length={PIN_LENGTH}
24
24
  mask
25
25
  required
26
- fullWidth
27
26
  error={error}
28
27
  onValueChange={() => setError(undefined)}
29
28
  />
30
- <Button.Root type="submit" fullWidth>
31
- Привязать карту
32
- </Button.Root>
29
+ <Button.Root type="submit">Привязать карту</Button.Root>
33
30
  </form>
34
31
  );
35
32
  }
@@ -1,4 +1,4 @@
1
- /** Every size; the cell is a square with the side of the control height — `size`. */
1
+ /** Every size; the cell is a square on its own scale, two steps above the control heights — `size`. */
2
2
  import { DigitInput } from "prime-ui-kit";
3
3
 
4
4
  const SIZES = ["xs", "s", "m", "l", "xl"] as const;
@@ -1,6 +1,6 @@
1
1
  # Divider
2
2
 
3
- **Category:** layout
3
+ **Category:** page
4
4
  **Kind:** primitive
5
5
 
6
6
  > A hairline separator inside one surface, horizontal or vertical, with or without a label.
@@ -1,6 +1,6 @@
1
1
  # Dnd
2
2
 
3
- **Category:** layout
3
+ **Category:** interaction
4
4
  **Kind:** composite
5
5
 
6
6
  > Pointer-driven drag and drop: reorderable lists, draggable items and drop zones on one shared session, with a lifted clone, a gap where the item lands, auto-scroll, touch support and a keyboard path.
@@ -131,10 +131,10 @@ Generic `Dnd.DropZone<TData>`; `ref` → `HTMLElement`. Renders the `as` element
131
131
  | State | Driven by | DOM |
132
132
  |---|---|---|
133
133
  | idle | — | items show `cursor: grab` |
134
- | carried | a drag | the clone follows the pointer, lifted on `--prime-shadow-modal`, scale 1.02; a sortable source leaves the layout (`data-lifted`) and a dashed accent gap of its size and radius fades in; neighbours glide aside; a `Dnd.Draggable` stays at 0.4 opacity (`data-dragging`) |
134
+ | carried | a drag | the clone follows the pointer, lifted on `--prime-shadow-modal`, scale 1.02; a sortable source leaves the layout (`data-lifted`) and a dashed accent gap of its size and radius fades in and glides with the items as the landing spot moves; entering a connected list, it fades out in the list it left and in where it opens; crossing empty space (the gutter between lists) leaves the gap in the last list that takes the item; neighbours glide aside; a `Dnd.Draggable` stays at 0.4 opacity (`data-dragging`) |
135
135
  | over a zone | pointer over an accepting zone | `data-dnd-over`: `accent-soft` fill, dashed accent outline |
136
136
  | refused | `canDrop` returns `false` | `data-dnd-reject`: `danger-soft` fill; the clone gets a dashed `danger` outline |
137
- | dropped | release | the clone glides to the item's new place and is swapped for the real element; `data-dnd-flash` with `flashOnDrop` |
137
+ | dropped | release over a target, or over empty space while a list holds the gap | the clone glides to the item's new place and is swapped for the real element; `data-dnd-flash` with `flashOnDrop` |
138
138
  | cancelled | Escape, a system-cancelled pointer | the clone flies back, nothing changes |
139
139
  | disabled | `disabled` | `data-disabled`, no `aria-roledescription` |
140
140
 
@@ -3,13 +3,14 @@
3
3
  **Category:** overlays
4
4
  **Kind:** overlay
5
5
 
6
- > A modal side panel that slides in from the edge: filters, forms and record details.
6
+ > A modal panel that slides in from the edge: filters, forms and record details at the side, a bottom sheet on a phone.
7
7
 
8
8
  ## When to use
9
9
  - Editing settings or a record without leaving the page.
10
10
  - Filters or navigation on narrow screens (`side="left"`).
11
11
  - Read-only details of a list row (order, user, event).
12
12
  - Long content (history, logs) that needs a tall scrolling area.
13
+ - A short task on a phone — pick a date, choose an action, fill two fields — in a bottom sheet (`side="bottom"`).
13
14
 
14
15
  ## When not to use
15
16
  - Confirmations and short decisions → use [Modal](../modal/COMPONENT.md).
@@ -27,6 +28,7 @@ import { Drawer } from "prime-ui-kit";
27
28
  Drawer.Root state and dismiss policy (no DOM)
28
29
  ├─ Drawer.Trigger clones its child; opens on click
29
30
  └─ Drawer.Content portal + scrim + role="dialog" panel at the edge
31
+ ├─ (grab handle) side="bottom" only: aria-hidden bar, a drag starts here
30
32
  ├─ Drawer.Header grid: [Icon] [Title + Description] [close Button]
31
33
  │ ├─ Drawer.Icon
32
34
  │ ├─ Drawer.Title <h2>
@@ -49,7 +51,7 @@ No DOM, no ref. State and dismiss policy.
49
51
  | `defaultOpen` | `boolean` | `false` | Initial visibility, uncontrolled. |
50
52
  | `onOpenChange` | `(open: boolean) => void` | — | Called on every open and close: trigger, close button, `Close`, Escape, scrim click, code. |
51
53
  | `closeOnEscape` | `boolean` | `true` | Escape closes the dialog. |
52
- | `closeOnOutsideClick` | `boolean` | `true` | A click on the scrim closes the dialog; turn off for destructive confirms. |
54
+ | `closeOnOutsideClick` | `boolean` | `true` | A click on the scrim (and a swipe on a bottom sheet) closes the dialog; turn off for destructive confirms. |
53
55
  | `labels` | `Partial<DrawerLabels>` | — | Built-in strings, see Labels. |
54
56
  | `children` | `ReactNode` | — | Trigger and Content. |
55
57
 
@@ -58,8 +60,8 @@ No DOM, no ref. State and dismiss policy.
58
60
 
59
61
  | Prop | Type | Default | Description |
60
62
  |---|---|---|---|
61
- | `side` | `"left" \| "right"` | `"right"` | Edge the panel slides from; rounded only on the edge facing the page. |
62
- | `size` | `"s" \| "m" \| "l" \| "xl"` | `"m"` | Width: 360 · 480 · 640 · 800. Below 640 px of viewport — full width, square corners. |
63
+ | `side` | `"left" \| "right" \| "bottom"` | `"right"` | Edge the panel slides from; rounded only on the edge facing the page. `bottom` is a sheet: a grab handle on top, height by content. A swipe toward the edge closes the panel (with `closeOnOutsideClick`): a bottom sheet from its handle or header, a side drawer by touch anywhere (by mouse from the header). |
64
+ | `size` | `"s" \| "m" \| "l" \| "xl"` | `"m"` | Width: 360 · 480 · 640 · 800; a bottom sheet is centred and at least 560 (`--prime-sheet-min-width`). Below 640 px of viewport — full width; side drawers lose their corners. |
63
65
  | `aria-label` | `string` | — | Dialog name when there is no Title. |
64
66
  | `aria-labelledby` | `string` | — | Overrides the Title id. |
65
67
  | `aria-describedby` | `string` | — | Overrides the Description id. |
@@ -94,7 +96,7 @@ No DOM, no ref. State and dismiss policy.
94
96
 
95
97
  | Prop | Type | Default | Description |
96
98
  |---|---|---|---|
97
- | `layout` | `"fill" \| "end"` | `"end"` | `fill`: equal-width buttons in one row; `end`: auto width, at the end. Narrower than 360 px — stacked. |
99
+ | `layout` | `"fill" \| "end"` | `"end"` | `fill`: equal-width buttons in one row; `end`: auto width, at the end. Stacked on phones (viewport below 640 px) and in a dialog narrower than 360 px. |
98
100
 
99
101
  ### Drawer.Trigger · Drawer.Close
100
102
  No DOM: clone the single child and chain its `onClick` (unless the child's handler calls `preventDefault()`).
@@ -110,6 +112,7 @@ No DOM: clone the single child and chain its `onClick` (unless the child's handl
110
112
  |---|---|---|---|
111
113
  | `right` | slides from the right, rounded on its left edge | details and forms | yes |
112
114
  | `left` | slides from the left, rounded on its right edge | filters, mobile navigation | |
115
+ | `bottom` | rises from the bottom, rounded on top, grab handle, height by content | a short task on a phone | |
113
116
 
114
117
  ### size (Content)
115
118
  | Value | Looks like | Use when | Default |
@@ -119,7 +122,7 @@ No DOM: clone the single child and chain its `onClick` (unless the child's handl
119
122
  | `l` | 640 wide | wide forms | |
120
123
  | `xl` | 800 wide | tables, previews | |
121
124
 
122
- Below 640px of viewport every size is full width with square corners.
125
+ A bottom sheet is centred at its width, never narrower than 560 (`--prime-sheet-min-width`), so a tablet gets a real sheet. Below 640px of viewport every size is full width; side drawers lose their corners, the sheet keeps its top radius.
123
126
 
124
127
  ### layout (Footer)
125
128
  | Value | Looks like | Use when | Default |
@@ -135,13 +138,15 @@ Below 640px of viewport every size is full width with square corners.
135
138
  |---|---|---|
136
139
  | open / closed | `open` / `defaultOpen` / `onOpenChange` | `data-state` on the scrim and the panel; mounted until the slide-out ends |
137
140
  | side / size | Content props | `data-side`, `data-size` on the panel |
141
+ | swiping | a drag toward the edge (with `closeOnOutsideClick`) | `data-swiping` and `--swipe-offset` on the panel while the pointer drags it; `data-swipe-dismissed` while it glides out after a closing release; `--swipe-progress` (0…1) on the scrim |
138
142
  | open: trapped | while open | focus trapped, page scroll locked, siblings of the portal `inert` (the toast region stays usable); a drawer opened from a Modal or another drawer stacks above it |
139
143
 
140
- Motion: the scrim fades, the panel slides from its side over `slow` and leaves over `base` (`overlayMotion`); under `prefers-reduced-motion` it unmounts at once.
144
+ Motion: the scrim fades, the panel slides from its side over `slow` and leaves over `base` (`overlayMotion`); under `prefers-reduced-motion` it unmounts at once. A swipe moves the panel with the pointer; on release it closes past 30% of its size or on a quick flick and glides the rest of the way out from where it was let go, otherwise it glides back — both over `base` on the `emphasized` (iOS sheet) curve. A pull the other way meets growing resistance and stops at 24; the panel's fill continues that far past the screen edge, so it never comes away from it. The scrim follows the panel: its fill thins in proportion to how far out the panel is, tracks the finger while dragging and glides with the panel on release. A bottom sheet drags from its handle or header; a side drawer by touch anywhere (not on text fields, sliders or sideways scrollers), by mouse from the header.
141
145
 
142
146
  ## Layout & spacing
143
147
  - Zone padding `--prime-drawer-padding`; hairlines between header, body and footer.
144
148
  - Full viewport height; only the body scrolls, with its own side padding.
149
+ - Bottom sheet: width by `size`, at least `--prime-sheet-min-width` (560); height by content up to `100dvh − --prime-sheet-top-gap`, radius `--prime-sheet-radius`, a `--prime-sheet-handle-area` strip with the handle bar, the bottom safe-area inset below the content.
145
150
  - Fields inside get the surface field fill.
146
151
 
147
152
  ## Accessibility
@@ -157,6 +162,7 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
157
162
  - Focus moves into the panel on open and returns to the opener on close, including after a scrim click.
158
163
  - The page behind is `inert`, scroll is locked; only the topmost layer reacts.
159
164
  - A drawer opened from a Modal stacks above it.
165
+ - A swipe toward the edge is an extra way out; the close button and Escape stay. The grab handle is `aria-hidden`.
160
166
 
161
167
  ### Labels
162
168
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -171,7 +177,8 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
171
177
  | [overview.tsx](examples/overview.tsx) | Order details next to the list: a trigger, a header with a close button and a read-only body — `Drawer.Trigger`, `Drawer.Body`. |
172
178
  | [structure.tsx](examples/structure.tsx) | Optional parts: an icon tile in the header and one full-width action in the footer — `Drawer.Icon`, `Drawer.Footer`, `layout`. |
173
179
  | [sizes.tsx](examples/sizes.tsx) | Every panel width, 360 to 800 px; below 640 px of viewport the panel takes the full width — `size`. |
174
- | [placement.tsx](examples/placement.tsx) | The panel slides from the right for details and from the left for filters — `side`. |
180
+ | [placement.tsx](examples/placement.tsx) | The panel slides from the right for details, from the left for filters and up from the bottom as a sheet — `side`. |
181
+ | [date-sheet.tsx](examples/date-sheet.tsx) | A date picked in a bottom sheet: a grab handle on top, a swipe down or a pick closes it — `side`, `Drawer.Body`. |
175
182
  | [long-content.tsx](examples/long-content.tsx) | A long body scrolls on its own while the header and the footer stay in place — `Drawer.Body`. |
176
183
  | [dismiss.tsx](examples/dismiss.tsx) | An import closes only from its buttons, and not at all while it runs — `closeOnOutsideClick`, `closeOnEscape`. |
177
184
  | [controlled-open.tsx](examples/controlled-open.tsx) | The parent owns the state and opens the drawer from a link, without a trigger — `open`, `onOpenChange`. |
@@ -180,7 +187,8 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
180
187
  ## Mistakes
181
188
  - A delete confirmation in a Drawer → use Modal.
182
189
  - Expecting Enter to submit via `Drawer.Confirm` → Drawer has no Confirm; use a `<form>` with a submit button.
183
- - `side="top"` / `"bottom"` → only `left` and `right`.
190
+ - `side="top"` → only `left`, `right` and `bottom`.
191
+ - Swipe as the only way to close → keep `Drawer.Header` with its close button (or `Drawer.Close`); a swipe is an extra gesture.
184
192
  - Padding on a wrapper inside Body → Body already pads and scrolls.
185
193
 
186
194
  ## Related
@@ -0,0 +1,41 @@
1
+ /** A date picked in a bottom sheet: a grab handle on top, a swipe down or a pick closes it — `side`, `Drawer.Body`. */
2
+ import { Button, Datepicker, Drawer, Icon } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const FORMAT = new Intl.DateTimeFormat("ru-RU", { day: "numeric", month: "long", year: "numeric" });
8
+
9
+ export default function DrawerDateSheetExample() {
10
+ const [open, setOpen] = React.useState(false);
11
+ const [date, setDate] = React.useState<Date | null>(new Date(2026, 2, 8));
12
+
13
+ return (
14
+ <Drawer.Root open={open} onOpenChange={setOpen}>
15
+ <Drawer.Trigger>
16
+ <Button.Root variant="soft" tone="neutral">
17
+ <Button.Icon>
18
+ <Icon name="field.calendar" />
19
+ </Button.Icon>
20
+ {date ? `Отгрузка ${FORMAT.format(date)}` : "Дата отгрузки"}
21
+ </Button.Root>
22
+ </Drawer.Trigger>
23
+ <Drawer.Content side="bottom">
24
+ <Drawer.Header>
25
+ <Drawer.Title>Дата отгрузки</Drawer.Title>
26
+ </Drawer.Header>
27
+ <Drawer.Body>
28
+ <Datepicker.Panel
29
+ mode="single"
30
+ className={styles.calendar}
31
+ value={date}
32
+ onValueChange={(next) => {
33
+ setDate(next);
34
+ setOpen(false);
35
+ }}
36
+ />
37
+ </Drawer.Body>
38
+ </Drawer.Content>
39
+ </Drawer.Root>
40
+ );
41
+ }
@@ -19,3 +19,8 @@
19
19
  margin: 0;
20
20
  font-variant-numeric: tabular-nums;
21
21
  }
22
+
23
+ /* date-sheet.tsx: the calendar keeps its own width, centred in the sheet. */
24
+ .calendar {
25
+ align-self: center;
26
+ }
@@ -1,9 +1,10 @@
1
- /** The panel slides from the right for details and from the left for filters — `side`. */
1
+ /** The panel slides from the right for details, from the left for filters and up from the bottom as a sheet — `side`. */
2
2
  import { Button, Drawer, Typography } from "prime-ui-kit";
3
3
 
4
4
  const SIDES = [
5
5
  { side: "left", title: "Фильтры" },
6
6
  { side: "right", title: "Детали заказа" },
7
+ { side: "bottom", title: "Действия с заказом" },
7
8
  ] as const;
8
9
 
9
10
  export default function DrawerPlacementExample() {
@@ -22,7 +23,8 @@ export default function DrawerPlacementExample() {
22
23
  </Drawer.Header>
23
24
  <Drawer.Body>
24
25
  <Typography variant="body-m" tone="secondary">
25
- Скругление только со стороны страницы, подложка затемняет остальное.
26
+ Скругление только со стороны страницы, подложка затемняет остальное. Свайп к краю
27
+ закрывает панель.
26
28
  </Typography>
27
29
  </Drawer.Body>
28
30
  </Drawer.Content>