prime-ui-kit 0.20.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunks/{chunk-4KONMO7Z.js → chunk-6L7VWEY6.js} +1 -1
- package/dist/chunks/{chunk-3PY52JP3.js → chunk-S27VR4ON.js} +5828 -5596
- package/dist/chunks/{chunk-4374EBWV.js → chunk-S4H2ZKWJ.js} +1049 -644
- package/dist/color-picker.js +2 -2
- package/dist/components/badge/Badge.d.ts +19 -2
- package/dist/components/card/Card.d.ts +10 -3
- package/dist/components/composer/Composer.d.ts +49 -1
- package/dist/components/crossfade/Crossfade.d.ts +6 -4
- package/dist/components/data-table/DataTable.d.ts +10 -2
- package/dist/components/data-table/Row.d.ts +12 -1
- package/dist/components/data-table/SelectionMenu.d.ts +20 -0
- package/dist/components/data-table/useRowSelection.d.ts +2 -0
- package/dist/components/empty-page/EmptyPage.d.ts +5 -2
- package/dist/components/hint/Hint.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/input/Input.d.ts +15 -3
- package/dist/components/message/Message.d.ts +6 -3
- package/dist/components/segmented-control/SegmentedControl.d.ts +5 -1
- package/dist/components/smart-filter/SmartFilter.d.ts +4 -0
- package/dist/components/spinner/Spinner.d.ts +3 -2
- package/dist/components/tabs/Tabs.d.ts +10 -1
- package/dist/hooks/useModalLayer.d.ts +15 -8
- package/dist/index.css +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/RegionSwap.d.ts +18 -10
- package/dist/internal/RollingNumber.d.ts +7 -5
- package/dist/internal/ToolRow.d.ts +6 -0
- package/dist/internal/mediaSlot.d.ts +29 -0
- package/dist/internal/overlay/DialogParts.d.ts +30 -6
- package/dist/internal/overlay/focus.d.ts +18 -3
- package/dist/internal/partPresence.d.ts +19 -2
- package/dist/layout/sidebar/Sidebar.d.ts +3 -2
- package/dist/skill/anti-slop.md +35 -37
- package/dist/skill/api-contract.md +50 -2
- package/dist/skill/cheatsheet.md +15 -10
- package/dist/skill/checklist.md +19 -6
- package/dist/skill/choosing.md +79 -6
- package/dist/skill/components.md +1 -1
- package/dist/skill/composition.md +211 -51
- package/dist/skill/foundations.md +17 -6
- package/dist/skill/layouts.md +64 -14
- package/dist/skill/motion.md +9 -3
- package/dist/skill/reference/components/avatar/COMPONENT.md +1 -0
- package/dist/skill/reference/components/badge/COMPONENT.md +29 -5
- package/dist/skill/reference/components/badge/examples/examples.module.css +14 -0
- package/dist/skill/reference/components/badge/examples/overflow-count.tsx +32 -0
- package/dist/skill/reference/components/banner/COMPONENT.md +1 -1
- package/dist/skill/reference/components/card/COMPONENT.md +8 -4
- package/dist/skill/reference/components/card/examples/person-header.tsx +38 -0
- package/dist/skill/reference/components/composer/COMPONENT.md +35 -5
- package/dist/skill/reference/components/composer/examples/bot-keyboard.tsx +43 -0
- package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
- package/dist/skill/reference/components/crossfade/COMPONENT.md +18 -10
- package/dist/skill/reference/components/data-table/COMPONENT.md +17 -11
- package/dist/skill/reference/components/data-table/examples/screen.tsx +6 -9
- package/dist/skill/reference/components/data-table/examples/selection.tsx +60 -18
- package/dist/skill/reference/components/drawer/COMPONENT.md +41 -19
- package/dist/skill/reference/components/drawer/examples/footer-actions.tsx +54 -0
- package/dist/skill/reference/components/drawer/examples/header-actions.tsx +46 -25
- package/dist/skill/reference/components/drawer/examples/link-loading.tsx +57 -0
- package/dist/skill/reference/components/drawer/examples/screen.tsx +3 -5
- package/dist/skill/reference/components/empty-page/COMPONENT.md +2 -2
- package/dist/skill/reference/components/field-list/COMPONENT.md +4 -4
- package/dist/skill/reference/components/field-list/examples/row-menu.tsx +1 -1
- package/dist/skill/reference/components/hint/COMPONENT.md +1 -1
- package/dist/skill/reference/components/input/COMPONENT.md +16 -3
- package/dist/skill/reference/components/input/examples/password-policy.tsx +65 -0
- package/dist/skill/reference/components/list/COMPONENT.md +1 -1
- package/dist/skill/reference/components/login-form/COMPONENT.md +13 -11
- package/dist/skill/reference/components/login-form/examples/overview.tsx +5 -5
- package/dist/skill/reference/components/login-form/examples/states.tsx +7 -6
- package/dist/skill/reference/components/message/COMPONENT.md +5 -5
- package/dist/skill/reference/components/message/examples/bot-actions.tsx +47 -28
- package/dist/skill/reference/components/modal/COMPONENT.md +31 -12
- package/dist/skill/reference/components/modal/examples/structure.tsx +2 -4
- package/dist/skill/reference/components/segmented-control/COMPONENT.md +11 -6
- package/dist/skill/reference/components/segmented-control/examples/router.tsx +22 -0
- package/dist/skill/reference/components/select/COMPONENT.md +4 -1
- package/dist/skill/reference/components/sheet/COMPONENT.md +31 -12
- package/dist/skill/reference/components/sheet/examples/header-actions.tsx +5 -3
- package/dist/skill/reference/components/skeleton/COMPONENT.md +3 -2
- package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -1
- package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +8 -2
- package/dist/skill/reference/components/spinner/COMPONENT.md +1 -1
- package/dist/skill/reference/components/tabs/COMPONENT.md +7 -4
- package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +18 -2
- package/dist/skill/reference/components/thumbnail/COMPONENT.md +1 -0
- package/dist/skill/reference/components/tooltip/COMPONENT.md +3 -3
- package/dist/skill/reference/foundation.md +15 -9
- package/dist/skill/reference/layout/page/COMPONENT.md +1 -0
- package/dist/skill/reference/layout/sidebar/COMPONENT.md +4 -2
- package/dist/skill/reference/layout/sidebar/examples/account-profile.tsx +66 -0
- package/dist/skill/responsive.md +28 -6
- package/dist/skill/ux.md +97 -2
- package/dist/tokens/semantic.d.ts +2 -1
- package/dist/tokens/themes/dark.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/avatar/COMPONENT.md +1 -0
- package/src/components/badge/COMPONENT.md +29 -5
- package/src/components/badge/examples/examples.module.css +14 -0
- package/src/components/badge/examples/overflow-count.tsx +32 -0
- package/src/components/banner/COMPONENT.md +1 -1
- package/src/components/card/COMPONENT.md +8 -4
- package/src/components/card/examples/person-header.tsx +38 -0
- package/src/components/composer/COMPONENT.md +35 -5
- package/src/components/composer/examples/bot-keyboard.tsx +43 -0
- package/src/components/confirm-dialog/COMPONENT.md +1 -1
- package/src/components/crossfade/COMPONENT.md +18 -10
- package/src/components/data-table/COMPONENT.md +17 -11
- package/src/components/data-table/examples/screen.tsx +6 -9
- package/src/components/data-table/examples/selection.tsx +60 -18
- package/src/components/drawer/COMPONENT.md +41 -19
- package/src/components/drawer/examples/footer-actions.tsx +54 -0
- package/src/components/drawer/examples/header-actions.tsx +46 -25
- package/src/components/drawer/examples/link-loading.tsx +57 -0
- package/src/components/drawer/examples/screen.tsx +3 -5
- package/src/components/empty-page/COMPONENT.md +2 -2
- package/src/components/field-list/COMPONENT.md +4 -4
- package/src/components/field-list/examples/row-menu.tsx +1 -1
- package/src/components/hint/COMPONENT.md +1 -1
- package/src/components/input/COMPONENT.md +16 -3
- package/src/components/input/examples/password-policy.tsx +65 -0
- package/src/components/list/COMPONENT.md +1 -1
- package/src/components/login-form/COMPONENT.md +13 -11
- package/src/components/login-form/examples/overview.tsx +5 -5
- package/src/components/login-form/examples/states.tsx +7 -6
- package/src/components/message/COMPONENT.md +5 -5
- package/src/components/message/examples/bot-actions.tsx +47 -28
- package/src/components/modal/COMPONENT.md +31 -12
- package/src/components/modal/examples/structure.tsx +2 -4
- package/src/components/segmented-control/COMPONENT.md +11 -6
- package/src/components/segmented-control/examples/router.tsx +22 -0
- package/src/components/select/COMPONENT.md +4 -1
- package/src/components/sheet/COMPONENT.md +31 -12
- package/src/components/sheet/examples/header-actions.tsx +5 -3
- package/src/components/skeleton/COMPONENT.md +3 -2
- package/src/components/smart-filter/COMPONENT.md +2 -1
- package/src/components/smart-filter/examples/ranges.tsx +8 -2
- package/src/components/spinner/COMPONENT.md +1 -1
- package/src/components/tabs/COMPONENT.md +7 -4
- package/src/components/tabs/examples/in-drawer.tsx +18 -2
- package/src/components/thumbnail/COMPONENT.md +1 -0
- package/src/components/tooltip/COMPONENT.md +3 -3
- package/src/layout/page/COMPONENT.md +1 -0
- package/src/layout/sidebar/COMPONENT.md +4 -2
- package/src/layout/sidebar/examples/account-profile.tsx +66 -0
- package/src/styles/theme-dark.css +1 -0
- package/src/styles/theme-light.css +1 -0
|
@@ -233,11 +233,11 @@ Content parts in short: `Message.Media` — a [Gallery](../gallery/COMPONENT.md)
|
|
|
233
233
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other attributes. |
|
|
234
234
|
|
|
235
235
|
### Message.Actions
|
|
236
|
-
`ref` → `HTMLDivElement`. A row of a bot's inline keyboard under the bubble: `Button.Root`
|
|
236
|
+
`ref` → `HTMLDivElement`. A row of a bot's inline keyboard under the bubble: `Button.Root` keys (`soft` `neutral` by default) share one line equally, long labels ellipsize. A link key gets the link mark; a key whose `onClick` returns a promise loads until it settles.
|
|
237
237
|
|
|
238
238
|
| Prop | Type | Default | Description |
|
|
239
239
|
|---|---|---|---|
|
|
240
|
-
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` — the `Button.Root`s of the row (`
|
|
240
|
+
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` — the `Button.Root`s of the row (`asChild` for a link); `className`. |
|
|
241
241
|
|
|
242
242
|
### Message.Menu
|
|
243
243
|
`ref` → `HTMLDivElement`. The context menu: a `Dropdown.Content` of the `Dropdown.Item`s you write; the kit adds «Выбрать» when the chat has a selection. Opens on right click, long press and from the keyboard.
|
|
@@ -363,7 +363,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
363
363
|
- `Message.Text` wraps anywhere; the meta stands at the end of its last line.
|
|
364
364
|
- Round controls (download, cancel, retry, play) are a Button `shape="circle"`: hover, press, focus ring and the touch area follow the circle. A file's action is its round tile (48): the transfer disk (`internal/transferDisk`, `xl`) with its ring, the retry or the download stand on it — never a separate button beside the name. Every transfer, either way (media, file, video note, voice), is that one disk.
|
|
365
365
|
- `Message.Typing` is the last row of a list; a chat puts it there itself (`Chat.Root typing`).
|
|
366
|
-
- **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and
|
|
366
|
+
- **A bot's inline keyboard** (`Message.Actions`): each part is a row under the bubble, `--prime-space-1` below it and between rows and keys, on the chat's own surface (outside the bubble's fill). The keyboard is as wide as the message: the column takes the wider of the bubble and the keyboard (up to the bubble's cap) and the bubble widens to it, so their edges line up. A row is one line, as in Telegram: its keys share it equally (`flex: 1 1 0`, each key `fullWidth`), and a label longer than its share ellipsizes. Keys keep the control radius; the last row's outer bottom corners take the bubble's (`--prime-message-radius`), so the keyboard closes the message. A key is `Button.Root` `variant="soft" tone="neutral"` at the host tier unless it sets its own: Telegram's button `style` maps onto `tone` — `primary` → `accent`, `success`, `danger` (`variant="solid"` for a filled key). A custom emoji before the label is a `Button.Icon`. A link key (`asChild` around an `<a href>`) carries the link mark (`action.externalLink`, `xs`) at its top-end corner, out of the label's flow. A callback key whose `onClick` returns a promise is `loading` until it settles: the spinner takes its leading icon's place or covers the label, the width never changes. A message with a keyboard has no tail: the keyboard closes it.
|
|
367
367
|
|
|
368
368
|
## Accessibility
|
|
369
369
|
|
|
@@ -506,7 +506,7 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
506
506
|
| [location.tsx](examples/location.tsx) | A place on a map picture from your map service with a pin, the place and its address, and a live location of a courier on its way — `Message.Location`, `href`, `live`. |
|
|
507
507
|
| [contact.tsx](examples/contact.tsx) | A shared contact: initials or a photo, name and phone, with the actions to write or save — `Message.Contact`, `phone`. |
|
|
508
508
|
| [link-preview.tsx](examples/link-preview.tsx) | A message with a link and the card of the page under it: site, title, description and picture — `Message.LinkPreview`, `site`, `image`. |
|
|
509
|
-
| [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: rows
|
|
509
|
+
| [bot-actions.tsx](examples/bot-actions.tsx) | A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, a link with its mark — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. |
|
|
510
510
|
| [sticker.tsx](examples/sticker.tsx) | Stickers in a conversation: a picture by its address and a video sticker in your own player, no bubble, the time on a pill over the corner — `Message.Sticker`, `src`, `loading`. |
|
|
511
511
|
| [preview.tsx](examples/preview.tsx) | A bot's welcome message as its settings page shows it: the same bubble with its keyboard, nothing in it pressed or opened, the time in the zone of the profile — `preview`, `timeZone`. |
|
|
512
512
|
| [narrow.tsx](examples/narrow.tsx) | At phone width bubbles take up to 85% of the chat, long words and links wrap inside, an album keeps its rows — `Message.Root`, `Message.Media`. |
|
|
@@ -534,5 +534,5 @@ The list-level settings a message reads — the group chat's author header and a
|
|
|
534
534
|
- A bot's buttons placed inside `Message.Text`, or a row of buttons of your own under a message → `Message.Actions` with `Button.Root` children: one row per part, at the message's width.
|
|
535
535
|
|
|
536
536
|
## Related
|
|
537
|
-
- **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown), [VisuallyHidden](../visually-hidden/COMPONENT.md)
|
|
537
|
+
- **Built from:** [Dropdown](../dropdown/COMPONENT.md) (`Message.Menu`), [ScrollContainer](../scroll-container/COMPONENT.md) (the floating panel of the reaction bar), [Gallery](../gallery/COMPONENT.md) (`Message.Media`), [Avatar](../avatar/COMPONENT.md) (`Avatar.Group` + `Avatar.Overflow` in reaction chips), [Badge](../badge/COMPONENT.md) (the «Трансляция» mark of a live location), [Thumbnail](../thumbnail/COMPONENT.md) (the map, the link preview picture, a file's preview), [Button](../button/COMPONENT.md) (retry, the transfer disk, `shape="circle"`, the buttons of `Message.Actions`), [Checkbox](../checkbox/COMPONENT.md) (`Checkbox.Indicator` in selection mode), [Spinner](../spinner/COMPONENT.md) (pending transfers and transcripts), [Skeleton](../skeleton/COMPONENT.md) (a sticker while it loads), Icon (status ticks, file glyphs, `action.forward`, `action.refresh`, `action.externalLink` — the link mark of a key); `internal/messagePart` (the contract of content parts), `internal/messageList` (the contract with a chat's list), `internal/palette` (`hueOf`), `internal/Disclosure` (the reactions row, transcripts), `bubbleMorph` (every size change as one stroke), `bubbleHug` (a bubble of text as wide as its widest line), `internal/transferDisk` (the transfer of a file), `internal/mediaPill` (the meta, the author's pill), `internal/HighlightMatch` (search marks), `internal/richText` (Markdown), [VisuallyHidden](../visually-hidden/COMPONENT.md)
|
|
538
538
|
- **See also:** [Chat](../chat/COMPONENT.md) — the conversation the messages stand in; [Gallery](../gallery/COMPONENT.md) — the album of `Message.Media`; [VideoNote](../video-note/COMPONENT.md) — the round video message; [VoicePlayer](../voice-player/COMPONENT.md) — the content of a voice message; [Composer](../composer/COMPONENT.md) — the input under the list; [RichText](../rich-text/COMPONENT.md) — the text of `Message.Text` outside a message; [Timeline](../timeline/COMPONENT.md)
|
|
@@ -1,51 +1,70 @@
|
|
|
1
|
-
/** A bot's message with an inline keyboard: rows
|
|
2
|
-
import { Button, Message } from "prime-ui-kit";
|
|
1
|
+
/** A bot's message with an inline keyboard: one-line rows whose keys share the row equally, a coloured key with an icon that loads while its promise runs, a link with its mark — `Message.Actions`, `tone`, `Button.Icon`, `asChild`. */
|
|
2
|
+
import { Button, Icon, Message } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
import styles from "./examples.module.css";
|
|
6
6
|
|
|
7
7
|
const INVOICE_URL = "https://vector.example/invoices/4821";
|
|
8
|
+
const CONFIRM_DELAY_MS = 1200;
|
|
8
9
|
|
|
9
10
|
export default function MessageBotActionsExample() {
|
|
10
|
-
const [
|
|
11
|
+
const [decision, setDecision] = React.useState<string | null>(null);
|
|
12
|
+
|
|
13
|
+
// A callback key: the bot answers after a round trip, the key loads until then.
|
|
14
|
+
const confirm = () =>
|
|
15
|
+
new Promise<void>((resolve) => {
|
|
16
|
+
window.setTimeout(() => {
|
|
17
|
+
setDecision("Отгрузка подтверждена, машина выезжает в 16:30.");
|
|
18
|
+
resolve();
|
|
19
|
+
}, CONFIRM_DELAY_MS);
|
|
20
|
+
});
|
|
11
21
|
|
|
12
22
|
return (
|
|
13
23
|
<div className={styles.thread}>
|
|
14
|
-
<Message.Root value="bot-order" time="15:02">
|
|
24
|
+
<Message.Root value="bot-order" time="15:02" edited={decision !== null}>
|
|
15
25
|
<Message.Author>Бот склада</Message.Author>
|
|
16
26
|
<Message.Text>
|
|
17
|
-
|
|
27
|
+
{decision ??
|
|
28
|
+
"Заказ № 4821 собран: 12 позиций на 184 300 ₽. Подтвердите отгрузку до 16:00."}
|
|
18
29
|
</Message.Text>
|
|
30
|
+
{decision === null ? (
|
|
31
|
+
<Message.Actions>
|
|
32
|
+
<Button.Root tone="success" onClick={confirm}>
|
|
33
|
+
<Button.Icon>
|
|
34
|
+
<Icon name="action.check" />
|
|
35
|
+
</Button.Icon>
|
|
36
|
+
Подтвердить
|
|
37
|
+
</Button.Root>
|
|
38
|
+
<Button.Root tone="danger" onClick={() => setDecision("Отгрузка отменена.")}>
|
|
39
|
+
Отменить
|
|
40
|
+
</Button.Root>
|
|
41
|
+
</Message.Actions>
|
|
42
|
+
) : null}
|
|
43
|
+
{decision === null ? (
|
|
44
|
+
<Message.Actions>
|
|
45
|
+
<Button.Root onClick={() => setDecision("Состав заказа открыт для правки.")}>
|
|
46
|
+
Изменить состав
|
|
47
|
+
</Button.Root>
|
|
48
|
+
<Button.Root onClick={() => setDecision("Отгрузка перенесена на завтра.")}>
|
|
49
|
+
Перенести
|
|
50
|
+
</Button.Root>
|
|
51
|
+
</Message.Actions>
|
|
52
|
+
) : null}
|
|
53
|
+
{decision === null ? (
|
|
54
|
+
<Message.Actions>
|
|
55
|
+
<Button.Root onClick={() => setDecision("Менеджер свяжется с вами.")}>
|
|
56
|
+
Связаться с менеджером
|
|
57
|
+
</Button.Root>
|
|
58
|
+
</Message.Actions>
|
|
59
|
+
) : null}
|
|
19
60
|
<Message.Actions>
|
|
20
|
-
<Button.Root
|
|
21
|
-
Подтвердить заказ
|
|
22
|
-
</Button.Root>
|
|
23
|
-
<Button.Root variant="soft" tone="neutral" onClick={() => setAnswer("Изменить")}>
|
|
24
|
-
Изменить
|
|
25
|
-
</Button.Root>
|
|
26
|
-
</Message.Actions>
|
|
27
|
-
<Message.Actions>
|
|
28
|
-
<Button.Root
|
|
29
|
-
variant="soft"
|
|
30
|
-
tone="neutral"
|
|
31
|
-
onClick={() => setAnswer("Связаться с менеджером")}
|
|
32
|
-
>
|
|
33
|
-
Связаться с менеджером
|
|
34
|
-
</Button.Root>
|
|
35
|
-
</Message.Actions>
|
|
36
|
-
<Message.Actions>
|
|
37
|
-
<Button.Root variant="soft" tone="neutral" asChild>
|
|
61
|
+
<Button.Root asChild>
|
|
38
62
|
<a href={INVOICE_URL} target="_blank" rel="noreferrer">
|
|
39
63
|
Открыть счёт
|
|
40
64
|
</a>
|
|
41
65
|
</Button.Root>
|
|
42
66
|
</Message.Actions>
|
|
43
67
|
</Message.Root>
|
|
44
|
-
{answer ? (
|
|
45
|
-
<Message.Root value="reply" direction="outgoing" time="15:03" status="sent">
|
|
46
|
-
<Message.Text>{answer}</Message.Text>
|
|
47
|
-
</Message.Root>
|
|
48
|
-
) : null}
|
|
49
68
|
</div>
|
|
50
69
|
);
|
|
51
70
|
}
|
|
@@ -29,13 +29,14 @@ Modal.Root state and dismiss policy (no DOM)
|
|
|
29
29
|
├── (grab handle) as a phone sheet with `closeOnOutsideClick`: aria-hidden bar, a drag starts here
|
|
30
30
|
├── Modal.Guard no DOM: holds the dialog (busy / dirty) from inside
|
|
31
31
|
├── Modal.Header grid: [Icon] [Title + Description] [Actions] [close button]
|
|
32
|
-
│ ├── Modal.Icon 40 tile with a tone fill;
|
|
33
|
-
│ ├── Modal.Title <h2>, names the dialog
|
|
34
|
-
│ ├── Modal.Description <p>, describes the dialog
|
|
35
|
-
│ └── Modal.Actions
|
|
32
|
+
│ ├── Modal.Icon 40 tile with a tone fill; the record's Avatar / Thumbnail with no tile, 40
|
|
33
|
+
│ ├── Modal.Title line 1: <h2>, names the dialog; `loading` holds it with a skeleton
|
|
34
|
+
│ ├── Modal.Description line 2: <p>, describes the dialog; status Badges lead it (tier s)
|
|
35
|
+
│ └── Modal.Actions buttons only (icon buttons, a ⋯ Dropdown), tier s, on the title's line
|
|
36
36
|
├── Modal.Body the only scrolling zone
|
|
37
37
|
│ └── Modal.Section <section>: <h3> label, description, Modal.Actions at the heading's end
|
|
38
38
|
└── Modal.Footer actions, primary last
|
|
39
|
+
├── Modal.Actions the start group: an action apart from the main pair
|
|
39
40
|
├── Modal.Meta <p>, muted status at the start
|
|
40
41
|
├── Modal.Close clones its child; closes on click
|
|
41
42
|
└── Modal.Confirm clones its child; Enter clicks it
|
|
@@ -84,19 +85,19 @@ No DOM, no ref. State and dismiss policy.
|
|
|
84
85
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
|
|
85
86
|
|
|
86
87
|
### Modal.Actions
|
|
87
|
-
`ref` → `HTMLDivElement`.
|
|
88
|
+
`ref` → `HTMLDivElement`. Header buttons only (icon buttons, a ⋯ Dropdown; a status goes in `Modal.Description`), tier `s`, on the title's line; under it below 12rem; at the end of a `Modal.Section` heading. In `Modal.Footer`: the start group at the footer tier.
|
|
88
89
|
|
|
89
90
|
| Prop | Type | Default | Description |
|
|
90
91
|
|---|---|---|---|
|
|
91
92
|
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the row. |
|
|
92
93
|
|
|
93
94
|
### Modal.Icon
|
|
94
|
-
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around an `Avatar.Root` or a `Thumbnail.Root`
|
|
95
|
+
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around the record's picture (an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own component) no tile (`data-media`), the picture at 40 px.
|
|
95
96
|
|
|
96
97
|
| Prop | Type | Default | Description |
|
|
97
98
|
|---|---|---|---|
|
|
98
99
|
| `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color; not used around a picture. |
|
|
99
|
-
| `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (
|
|
100
|
+
| `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (size `l`) or a `Thumbnail.Root` (size `m`), set by the slot. |
|
|
100
101
|
| `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
|
|
101
102
|
|
|
102
103
|
### Modal.Title
|
|
@@ -104,10 +105,11 @@ No DOM, no ref. State and dismiss policy.
|
|
|
104
105
|
|
|
105
106
|
| Prop | Type | Default | Description |
|
|
106
107
|
|---|---|---|---|
|
|
108
|
+
| `loading` | `boolean` | `false` | The name loads (a record opened by a link): a `Skeleton` holds the title line and `aria-busy` is set, so the header never moves; the name fades in. |
|
|
107
109
|
| `…rest` | `Omit<HTMLAttributes<HTMLHeadingElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
|
|
108
110
|
|
|
109
111
|
### Modal.Description
|
|
110
|
-
`ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog.
|
|
112
|
+
`ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog. The record's status `Badge.Root`s go here: first in the line, chip tier `s` (20 px), the text wraps under them when narrow.
|
|
111
113
|
|
|
112
114
|
| Prop | Type | Default | Description |
|
|
113
115
|
|---|---|---|---|
|
|
@@ -131,7 +133,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
131
133
|
| `…rest` | `Omit<HTMLAttributes<HTMLElement>, "title">` | — | `children` (the content and an optional `Modal.Actions`), `className` and the other attributes of the `<section>`. |
|
|
132
134
|
|
|
133
135
|
### Modal.Footer
|
|
134
|
-
`ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last
|
|
136
|
+
`ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last; at the start an optional `Modal.Actions` (an action apart from the main pair) and `Meta`. Stacked, the start comes first and the primary stays last.
|
|
135
137
|
|
|
136
138
|
| Prop | Type | Default | Description |
|
|
137
139
|
|---|---|---|---|
|
|
@@ -198,7 +200,7 @@ With a `Modal.Meta` the footer is a row in any layout: the status takes the free
|
|
|
198
200
|
| `danger` | `danger-soft` tile, danger icon | A warning about a risky step | |
|
|
199
201
|
| `info` | `info-soft` tile, info icon | Informational notices | |
|
|
200
202
|
|
|
201
|
-
Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root`
|
|
203
|
+
Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own wrapper component — there is no tile and no tone: `data-media`, and the picture takes the tile's 40 px (avatar `l`, thumbnail `m`) whatever `size` it was given.
|
|
202
204
|
|
|
203
205
|
### Flags
|
|
204
206
|
| Value | Looks like | Use when | Default |
|
|
@@ -227,8 +229,11 @@ Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` (found by
|
|
|
227
229
|
| busy | `Modal.Guard busy` | Escape, the scrim, a swipe, the close button and `Modal.Close` do nothing; the close button and the `Close` child are `disabled` in place |
|
|
228
230
|
| dirty | `Modal.Guard dirty` | a dismiss opens the «Не сохранять изменения?» ConfirmDialog (`labels.discard*`); its confirm closes the dialog; a swipe past the threshold glides back and asks the same |
|
|
229
231
|
| leaving the tab | `Modal.Guard busy` or `dirty` | the browser asks before the tab closes or reloads (`beforeunload`), as with `Page.Guard` |
|
|
232
|
+
| title loading | `Modal.Title loading` | a `Skeleton` holds the title line, `aria-busy` on the `<h2>`; the name fades in under the same element |
|
|
230
233
|
| description loading | `Modal.Description loading` | a `Skeleton` holds the line, `aria-busy` on the `<p>`; the data fades in under the same element |
|
|
231
234
|
| actions below | the title would get less than 12rem beside `Modal.Actions` | `data-actions-below` on the header: the actions move under the title and the description |
|
|
235
|
+
| picture mark | an `Avatar.Root` or a `Thumbnail.Root` inside `Modal.Icon` | `data-media` on the icon, no tile; the picture's `data-size` is the slot's (`l` / `m`) |
|
|
236
|
+
| status in the line | a `Badge.Root` inside `Modal.Description` | the line is a wrapping flex row, the badges first (`order`), each `data-tier="s"` |
|
|
232
237
|
| sheet | a centred dialog on a phone viewport | `data-sheet` on the dialog: the bottom-sheet surface, the grab handle while it swipes |
|
|
233
238
|
| swiping | as a phone sheet, a drag down from the handle or the header (with `closeOnOutsideClick`; without it there is no handle and no swipe) | `data-swiping` and `--swipe-offset` on the dialog while the pointer drags it; past 30% of its height or on a flick it closes, otherwise it glides back; a busy `Modal.Guard` sets `data-swipe-disabled` and the handle takes no drag |
|
|
234
239
|
| placement | `placement` on Content | `data-placement` on the dialog |
|
|
@@ -238,6 +243,16 @@ Loading or disabled actions use the Button props (`loading`, `disabled`) inside
|
|
|
238
243
|
Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fades and scales from 0.98 over `base` and leaves over `fast`; as a bottom sheet it slides up over `slow` and down over `base`. Content stays mounted until the exit ends; under `prefers-reduced-motion` it unmounts at once.
|
|
239
244
|
|
|
240
245
|
## Layout & spacing
|
|
246
|
+
- The header has one anatomy, the same in every modal, drawer and sheet:
|
|
247
|
+
```
|
|
248
|
+
[mark] Title [icon buttons] [⋯] [×]
|
|
249
|
+
[status badges] description text
|
|
250
|
+
```
|
|
251
|
+
- mark — `Modal.Icon`: a glyph on the tone tile, or the record's picture (`Avatar.Root`, `Thumbnail.Root`, also inside your own wrapper component) with no tile; both are 40 px.
|
|
252
|
+
- line 1 — `Modal.Title`; `loading` holds it with a Skeleton while the name loads.
|
|
253
|
+
- line 2 — `Modal.Description`: the record's statuses as `Badge.Root`s first, then the text. The kit sizes the badges (chip tier `s`, the line's 20 px), keeps them first and one gap apart, and wraps the text under them on a narrow panel.
|
|
254
|
+
- end — `Modal.Actions`: only buttons — ghost icon buttons with a Tooltip and a ⋯ Dropdown — at the close button's tier, centred on the title's line; a status never goes here.
|
|
255
|
+
- An action apart from the main pair («Удалить» of a record, «Выйти»): `Modal.Actions` inside `Modal.Footer` puts it at the start, at the footer's tier; stacked on a phone it comes first and the primary stays last.
|
|
241
256
|
- Dialog padding `--prime-modal-padding` (24) on every zone; header 24 top / 20 bottom, body 20 with a 16 gap between blocks, footer 20 top / 24 bottom.
|
|
242
257
|
- Max height = viewport minus `--prime-modal-viewport-padding` on both sides; only the body scrolls.
|
|
243
258
|
- Tabs in the header: a `Tabs.List` inside `Modal.Header` (with `Tabs.Root` around the header and the body) spans the header's last row. The header becomes the tab strip, edge to edge and without the hairline; the active tab is the body rising into it, the panels sit in the body.
|
|
@@ -282,7 +297,7 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
|
|
|
282
297
|
| Example | Shows |
|
|
283
298
|
|---|---|
|
|
284
299
|
| [overview.tsx](examples/overview.tsx) | A trigger opens a dialog with a title, a field and two actions; Enter presses the confirm — `Modal.Trigger`, `Modal.Confirm`. |
|
|
285
|
-
| [structure.tsx](examples/structure.tsx) | Optional parts: an icon tile with a header-only notice, and a header
|
|
300
|
+
| [structure.tsx](examples/structure.tsx) | Optional parts: an icon tile with a header-only notice, and a header whose status leads the line under the title, with a footer and no body between them — `Modal.Icon`, `Modal.Description`, `Modal.Footer`. |
|
|
286
301
|
| [sizes.tsx](examples/sizes.tsx) | Every width; xs, s and m fill the footer with equal buttons, l and xl align them to the end — `size`. |
|
|
287
302
|
| [long-content.tsx](examples/long-content.tsx) | A long body in titled sections scrolls on its own while the header and the footer stay in place; the primary closes the dialog and is the Enter target — `Modal.Body`, `Modal.Section`, `Modal.Confirm`. |
|
|
288
303
|
| [custom-container.tsx](examples/custom-container.tsx) | The dialog portals into a given node instead of the document body — `container`. |
|
|
@@ -301,9 +316,13 @@ Motion (`overlayMotion`): the scrim fades in and out over `base`; the dialog fad
|
|
|
301
316
|
- A hand-made heading row (`Typography as="h3"` + buttons) over a group in the body → `Modal.Section label` with `Modal.Actions`.
|
|
302
317
|
- Busy or unsaved state lifted out of the form into Root props → put `Modal.Guard` next to the form, inside Content.
|
|
303
318
|
- `key` or a conditional around `Modal.Root` to reset a form → the dialog unmounts at once, without its exit; keep the Root mounted and drive `open`.
|
|
319
|
+
- A status `Badge` in the header's `Modal.Actions`, beside the buttons → put it in `Modal.Description`, before the text: the kit sizes it to the line and keeps it first.
|
|
320
|
+
- A Badge with its own `size` in `Modal.Description` → leave `size` out: the line gives every header the same chip tier.
|
|
321
|
+
- A `Skeleton` placed by hand in `Modal.Title` or `Modal.Description` → their `loading`.
|
|
322
|
+
- A start-side button («Выйти», «Удалить») put inside `Modal.Meta` → `Modal.Actions` inside `Modal.Footer`; `Meta` is status text only.
|
|
304
323
|
- Scrolling wrapper inside Modal.Body → Body already scrolls.
|
|
305
324
|
- Long multi-section forms in a modal → use Drawer.
|
|
306
325
|
|
|
307
326
|
## Related
|
|
308
|
-
- **Built from:** the dialog shared with Drawer and Sheet (`internal/overlay/DialogParts`, `internal/overlay/ModalDialog`), [Button](../button/COMPONENT.md) (header close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Modal.Body`),
|
|
327
|
+
- **Built from:** the dialog shared with Drawer and Sheet (`internal/overlay/DialogParts`, `internal/overlay/ModalDialog`), [Button](../button/COMPONENT.md) (header close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Modal.Body`), `internal/mediaSlot` (a record's Avatar or Thumbnail in `Modal.Icon`), [Skeleton](../skeleton/COMPONENT.md) (`Modal.Description loading`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Modal.Guard` discard confirm), `Icon` (the close glyph)
|
|
309
328
|
- **See also:** [ConfirmDialog](../confirm-dialog/COMPONENT.md) (a destructive confirm built on Modal `s`), [Drawer](../drawer/COMPONENT.md) (the same header, body, footer and `Guard` at the edge), [Popover](../popover/COMPONENT.md), [CommandMenu](../command-menu/COMPONENT.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Optional parts: an icon tile with a header-only notice, and a header
|
|
1
|
+
/** Optional parts: an icon tile with a header-only notice, and a header whose status leads the line under the title, with a footer and no body between them — `Modal.Icon`, `Modal.Description`, `Modal.Footer`. */
|
|
2
2
|
import { Badge, Button, Icon, Modal } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
export default function ModalStructureExample() {
|
|
@@ -33,11 +33,9 @@ export default function ModalStructureExample() {
|
|
|
33
33
|
<Modal.Header>
|
|
34
34
|
<Modal.Title>Опубликовать «Бизнес 2026»?</Modal.Title>
|
|
35
35
|
<Modal.Description>
|
|
36
|
+
<Badge.Root color="gray">Черновик</Badge.Root>
|
|
36
37
|
Новые клиенты увидят тариф сразу после публикации.
|
|
37
38
|
</Modal.Description>
|
|
38
|
-
<Modal.Actions>
|
|
39
|
-
<Badge.Root color="gray">Черновик</Badge.Root>
|
|
40
|
-
</Modal.Actions>
|
|
41
39
|
</Modal.Header>
|
|
42
40
|
<Modal.Footer>
|
|
43
41
|
<Modal.Close>
|
|
@@ -25,8 +25,8 @@ import { SegmentedControl } from "prime-ui-kit";
|
|
|
25
25
|
## Anatomy
|
|
26
26
|
```
|
|
27
27
|
SegmentedControl.Root field frame: label row · track · support row (hint | error)
|
|
28
|
-
└─ (track) role="radiogroup"; sliding thumb, switcher row, keyboard
|
|
29
|
-
└─ SegmentedControl.Item role="radio" button, one option
|
|
28
|
+
└─ (track) role="radiogroup" (role="navigation" with link items); sliding thumb, switcher row, keyboard
|
|
29
|
+
└─ SegmentedControl.Item role="radio" button, one option; the consumer's link with `asChild`
|
|
30
30
|
├─ SegmentedControl.Icon icon slot (optional; icon-only → square segment)
|
|
31
31
|
├─ SegmentedControl.Label title, one line (optional; plain text becomes the label)
|
|
32
32
|
├─ SegmentedControl.Count counter Badge after the label (optional)
|
|
@@ -63,13 +63,14 @@ The root is a field root: `className`, `ref` and the native attributes land on t
|
|
|
63
63
|
| `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "id" \| "children" \| "defaultValue" \| "defaultChecked" \| "onChange" \| "dir">` | — | `aria-label` / `aria-labelledby` / `aria-describedby` and `onKeyDown` go to the radiogroup (a prevented key skips the kit's handling); `className`, `data-*` and the other attributes to the field frame `<div>`. |
|
|
64
64
|
|
|
65
65
|
### SegmentedControl.Item
|
|
66
|
-
`ref` → `HTMLButtonElement`. One option, a `<button role="radio">` with roving `tabIndex`; plain text is wrapped in a `SegmentedControl.Label`.
|
|
66
|
+
`ref` → `HTMLButtonElement`, or the child element with `asChild`. One option, a `<button role="radio">` with roving `tabIndex`; plain text is wrapped in a `SegmentedControl.Label`. With `asChild` the segment is the consumer's link.
|
|
67
67
|
|
|
68
68
|
| Prop | Type | Default | Description |
|
|
69
69
|
|---|---|---|---|
|
|
70
70
|
| `value` | `string` | — (required) | Option value compared with the root value. |
|
|
71
71
|
| `disabled` | `boolean` | `false` | Disables this option; arrow keys skip it. |
|
|
72
72
|
| `color` | `"gray" \| "blue" \| "green" \| "orange" \| "red" \| "yellow" \| "purple" \| "sky" \| "pink" \| "teal"` | — | Palette hue: a dot before the content; while selected the thumb takes a soft fill of the hue with a ring of it. |
|
|
73
|
+
| `asChild` | `boolean` | `false` | The single child (a router `<Link>`) becomes the segment. A row of links is a navigation: links in the Tab order, the one matching `value` current, no arrow keys. |
|
|
73
74
|
| `children` | `ReactNode` | — (required) | Text, `Icon`, `Label`, `Count`, `Description`. Only an icon → square segment; give it `aria-label`. |
|
|
74
75
|
| `className` | `string` | — | Class on the segment button. |
|
|
75
76
|
| `…rest` | `Omit<ButtonHTMLAttributes<HTMLButtonElement>, "value" \| "type" \| "role">` | — | `aria-label`, `onClick` (runs first and can `preventDefault()` the selection), `className` and the other button attributes. |
|
|
@@ -141,6 +142,7 @@ Track is `fill-muted` with a faint 1px ring; the selected segment is a floating
|
|
|
141
142
|
|---|---|---|---|
|
|
142
143
|
| `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 |
|
|
143
144
|
| `disabled` | every segment disabled, thumb `fill-muted-hover` without shadow | the choice is temporarily unavailable | off |
|
|
145
|
+
| `asChild` (Item) | the same segment; the consumer's element (a router `<Link>`) renders it | route sections («Сотрудники / Роли») that must be real links | off |
|
|
144
146
|
| icon-only (only `SegmentedControl.Icon`) | square segment (`data-icon-only`) | toolbar view toggles; needs `aria-label` | — |
|
|
145
147
|
|
|
146
148
|
Colour all items or none; do not give an item and its Count the same hue; prefer content width or two-line segments over `fullWidth` with many long labels.
|
|
@@ -157,6 +159,7 @@ Colour all items or none; do not give an item and its Count the same hue; prefer
|
|
|
157
159
|
| required | `required` | red `*` after the label, `aria-required="true"` on the radiogroup |
|
|
158
160
|
| optional | `optional` | a muted `labels.optional` marker after the label |
|
|
159
161
|
| support row reserved | `reserveSupportRow` | the support row stays rendered while empty, so an error arriving shifts nothing |
|
|
162
|
+
| link row | an Item with `asChild` | track `role="navigation"`; each link in the Tab order, no arrow-key selection; the segment matching `value` `aria-current="page"` and `data-state="checked"` |
|
|
160
163
|
| focus-visible | keyboard | focus ring inside the segment |
|
|
161
164
|
| compact | row wider than the container at the tier's padding | row `data-collapse="compact"`: single-line segments tighten their padding by one step, so a row a few pixels too wide keeps its last segment and count whole; if it still does not fit (and not every segment has an icon) it scrolls |
|
|
162
165
|
| collapsed | even the compact row does not fit and every segment has an icon | row `data-collapse="icon"`: titles, counts and descriptions leave the row, segments turn square; a `VisuallyHidden` copy of the title names each segment and a `Tooltip` shows it |
|
|
@@ -180,10 +183,10 @@ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `dat
|
|
|
180
183
|
|
|
181
184
|
| Key | Action |
|
|
182
185
|
|---|---|
|
|
183
|
-
| `ArrowRight` · `ArrowDown` | Next option, wrapping and skipping disabled ones; focus and selection move together. |
|
|
186
|
+
| `ArrowRight` · `ArrowDown` | Next option, wrapping and skipping disabled ones; focus and selection move together (not in a row of links). |
|
|
184
187
|
| `ArrowLeft` · `ArrowUp` | Previous option. |
|
|
185
188
|
| `Home` · `End` | First and last enabled option. |
|
|
186
|
-
| `Tab` | Enters the group on the selected option (or the first enabled one) and leaves it. |
|
|
189
|
+
| `Tab` | Enters the group on the selected option (or the first enabled one) and leaves it; in a row of links it moves from link to link. |
|
|
187
190
|
|
|
188
191
|
### ARIA
|
|
189
192
|
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
@@ -191,6 +194,7 @@ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `dat
|
|
|
191
194
|
- The track is `role="radiogroup"`, named by `label` (`aria-labelledby`); without a label give the root `aria-label`. `aria-disabled` when disabled.
|
|
192
195
|
- Hint and error are linked through `aria-describedby`; an error sets `aria-invalid="true"`, `required` sets `aria-required="true"`.
|
|
193
196
|
- Items are `<button role="radio" aria-checked>` with roving `tabIndex`.
|
|
197
|
+
- With an `asChild` item the track is `role="navigation"` (named the same way), the links keep their role and the current one has `aria-current="page"`; `required` and `invalid` do not apply.
|
|
194
198
|
- Two-line items: `aria-labelledby` = label (+ count), `aria-describedby` = description; plain text is the label, so every reference resolves.
|
|
195
199
|
- Icons are `aria-hidden`; icon-only items need `aria-label` (and a Tooltip for sighted users). In a row collapsed to icons a `VisuallyHidden` copy of the title names the segment and a `Tooltip` shows it.
|
|
196
200
|
|
|
@@ -212,6 +216,7 @@ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `dat
|
|
|
212
216
|
| [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`. |
|
|
213
217
|
| [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 — `defaultValue`. |
|
|
214
218
|
| [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`. |
|
|
219
|
+
| [router.tsx](examples/router.tsx) | Router links as the segments: the current route is the value, each segment is a link and the row is a navigation; render it inside a router — `asChild`, `value`. |
|
|
215
220
|
| [controlled.tsx](examples/controlled.tsx) | The parent owns the choice and updates other content from it — `value`, `onValueChange`. |
|
|
216
221
|
| [in-form.tsx](examples/in-form.tsx) | An expense form: the choice is a labelled field with a hint, checked on submit, its error leaves as soon as a segment is chosen; an optional choice beside it — `label`, `hint`, `required`, `error`, `optional`. |
|
|
217
222
|
|
|
@@ -225,5 +230,5 @@ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `dat
|
|
|
225
230
|
- Expecting segments to wrap on narrow screens → they scroll; use `fullWidth` or fewer options.
|
|
226
231
|
|
|
227
232
|
## Related
|
|
228
|
-
- **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (`internal/SwitcherScroller`, the row), [Badge](../badge/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md) (the title of a collapsed segment), [Label](../label/COMPONENT.md) (`label`), [Hint](../hint/COMPONENT.md) (`hint`, `error`), [VisuallyHidden](../visually-hidden/COMPONENT.md); `internal/selectionIndicator` (the thumb)
|
|
233
|
+
- **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (`internal/SwitcherScroller`, the row), [Badge](../badge/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md) (the title of a collapsed segment), [Label](../label/COMPONENT.md) (`label`), [Hint](../hint/COMPONENT.md) (`hint`, `error`), [VisuallyHidden](../visually-hidden/COMPONENT.md); `internal/selectionIndicator` (the thumb), `internal/interactiveElement` (a link segment, `asChild`)
|
|
229
234
|
- **See also:** [Tabs](../tabs/COMPONENT.md), [Radio](../radio/COMPONENT.md), [Switch](../switch/COMPONENT.md), [ButtonGroup](../button-group/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Router links as the segments: the current route is the value, each segment is a link and the row is a navigation; render it inside a router — `asChild`, `value`. */
|
|
2
|
+
import { SegmentedControl } from "prime-ui-kit";
|
|
3
|
+
import { Link, useLocation } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
const SECTIONS = [
|
|
6
|
+
{ to: "/", label: "Сотрудники" },
|
|
7
|
+
{ to: "/roles", label: "Роли" },
|
|
8
|
+
{ to: "/invites", label: "Приглашения" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export default function SegmentedControlRouterExample() {
|
|
12
|
+
const location = useLocation();
|
|
13
|
+
return (
|
|
14
|
+
<SegmentedControl.Root value={location.pathname} aria-label="Команда и роли">
|
|
15
|
+
{SECTIONS.map((section) => (
|
|
16
|
+
<SegmentedControl.Item key={section.to} value={section.to} asChild>
|
|
17
|
+
<Link to={section.to}>{section.label}</Link>
|
|
18
|
+
</SegmentedControl.Item>
|
|
19
|
+
))}
|
|
20
|
+
</SegmentedControl.Root>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -238,7 +238,10 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
|
|
|
238
238
|
- A rich row and its trigger use the same parts in the same order: leading media (`Avatar.Root`,
|
|
239
239
|
`Thumbnail.Root`) → `Select.ItemText` (+ `Select.ItemDescription`) → trailing `Badge.Root` /
|
|
240
240
|
`Select.ItemMeta`. In a row the trailing part sits before the check; in the trigger, at the end
|
|
241
|
-
before the chevron.
|
|
241
|
+
before the chevron. Every single-choice row keeps the check's column, chosen or not, so the
|
|
242
|
+
trailing parts of all rows end on one line, under the trailing part of the trigger (the check
|
|
243
|
+
column stands where the chevron does), and picking another option moves no badge or price.
|
|
244
|
+
Draw the trigger with `renderValue` returning the row's parts, so the picked
|
|
242
245
|
person keeps their avatar and role badge when the list is closed:
|
|
243
246
|
|
|
244
247
|
```tsx
|
|
@@ -29,12 +29,13 @@ Sheet.Root state and dismiss policy (no DOM)
|
|
|
29
29
|
├─ (grab handle) aria-hidden bar on top while the sheet swipes (`closeOnOutsideClick`); a drag starts here
|
|
30
30
|
├─ Sheet.Guard no DOM: holds the sheet (busy / dirty) from inside
|
|
31
31
|
├─ Sheet.Header optional: [Icon] [Title + Description] [Actions] [close Button]
|
|
32
|
-
│ ├─ Sheet.Icon
|
|
33
|
-
│ ├─ Sheet.Title <h2
|
|
34
|
-
│ ├─ Sheet.Description <p
|
|
35
|
-
│ └─ Sheet.Actions
|
|
32
|
+
│ ├─ Sheet.Icon tone tile around an Icon, or the record's Avatar / Thumbnail, no tile, 40
|
|
33
|
+
│ ├─ Sheet.Title line 1: <h2>; `loading` holds it with a skeleton
|
|
34
|
+
│ ├─ Sheet.Description line 2: <p>; the record's status Badges lead it (tier s), then the text
|
|
35
|
+
│ └─ Sheet.Actions buttons only (icon buttons, a ⋯ Dropdown), tier s, on the title's line
|
|
36
36
|
├─ Sheet.Body the content; the only scrolling zone (ScrollContainer)
|
|
37
37
|
└─ Sheet.Footer optional: only for a form with an explicit submit
|
|
38
|
+
├─ Sheet.Actions the start group: an action apart from the main pair
|
|
38
39
|
├─ Sheet.Meta <p>, muted status at the start
|
|
39
40
|
└─ Sheet.Close clones its child; closes on click
|
|
40
41
|
```
|
|
@@ -77,19 +78,19 @@ No DOM, no ref. State and dismiss policy.
|
|
|
77
78
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
|
|
78
79
|
|
|
79
80
|
### Sheet.Actions
|
|
80
|
-
`ref` → `HTMLDivElement`.
|
|
81
|
+
`ref` → `HTMLDivElement`. Header buttons only (icon buttons, a ⋯ Dropdown; a status goes in `Sheet.Description`), tier `s`, on the title's line; under it below 12rem. In `Sheet.Footer`: the start group at the footer tier.
|
|
81
82
|
|
|
82
83
|
| Prop | Type | Default | Description |
|
|
83
84
|
|---|---|---|---|
|
|
84
85
|
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the row. |
|
|
85
86
|
|
|
86
87
|
### Sheet.Icon
|
|
87
|
-
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around an `Avatar.Root` or a `Thumbnail.Root`
|
|
88
|
+
`ref` → `HTMLSpanElement`. An `aria-hidden` 40 px tile with a tone fill; around the record's picture (an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own component) no tile (`data-media`), the picture at 40 px.
|
|
88
89
|
|
|
89
90
|
| Prop | Type | Default | Description |
|
|
90
91
|
|---|---|---|---|
|
|
91
92
|
| `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Soft fill and icon color; not used around a picture. |
|
|
92
|
-
| `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (
|
|
93
|
+
| `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (size `l`) or a `Thumbnail.Root` (size `m`), set by the slot. |
|
|
93
94
|
| `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
|
|
94
95
|
|
|
95
96
|
### Sheet.Title
|
|
@@ -97,10 +98,11 @@ No DOM, no ref. State and dismiss policy.
|
|
|
97
98
|
|
|
98
99
|
| Prop | Type | Default | Description |
|
|
99
100
|
|---|---|---|---|
|
|
101
|
+
| `loading` | `boolean` | `false` | The name loads (a record opened by a link): a `Skeleton` holds the title line and `aria-busy` is set, so the header never moves; the name fades in. |
|
|
100
102
|
| `…rest` | `Omit<HTMLAttributes<HTMLHeadingElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
|
|
101
103
|
|
|
102
104
|
### Sheet.Description
|
|
103
|
-
`ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog.
|
|
105
|
+
`ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog. The record's status `Badge.Root`s go here: first in the line, chip tier `s` (20 px), the text wraps under them when narrow.
|
|
104
106
|
|
|
105
107
|
| Prop | Type | Default | Description |
|
|
106
108
|
|---|---|---|---|
|
|
@@ -115,7 +117,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
115
117
|
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the zone. |
|
|
116
118
|
|
|
117
119
|
### Sheet.Footer
|
|
118
|
-
`ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last
|
|
120
|
+
`ref` → `HTMLElement`. Renders `<footer>` with the actions, primary last; at the start an optional `Sheet.Actions` (an action apart from the main pair) and `Meta`. Stacked, the start comes first and the primary stays last.
|
|
119
121
|
|
|
120
122
|
| Prop | Type | Default | Description |
|
|
121
123
|
|---|---|---|---|
|
|
@@ -161,7 +163,7 @@ Sheet has no `variant`, `side` or `size`: it always rises from the bottom edge,
|
|
|
161
163
|
| `fill` | equal-width buttons in one row | two equal answers | |
|
|
162
164
|
|
|
163
165
|
### tone (Icon)
|
|
164
|
-
`neutral` (default) · `accent` · `success` · `warning` · `danger` · `info` — soft tile fill and icon color, as in Modal. Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root`
|
|
166
|
+
`neutral` (default) · `accent` · `success` · `warning` · `danger` · `info` — soft tile fill and icon color, as in Modal. Around the record's picture — an `Avatar.Root` or a `Thumbnail.Root` anywhere inside, also from your own wrapper component — there is no tile and no tone: `data-media`, and the picture takes the tile's 40 px (avatar `l`, thumbnail `m`) whatever `size` it was given.
|
|
165
167
|
|
|
166
168
|
## States
|
|
167
169
|
| State | Driven by | DOM |
|
|
@@ -174,8 +176,11 @@ Sheet has no `variant`, `side` or `size`: it always rises from the bottom edge,
|
|
|
174
176
|
| busy | `Sheet.Guard busy` | Escape, the scrim, a swipe, the close button and `Sheet.Close` do nothing; the close button and the `Close` child are `disabled` in place; the handle stays, `data-swipe-disabled` on the panel (no grab cursor) |
|
|
175
177
|
| dirty | `Sheet.Guard dirty` | a dismiss opens the «Не сохранять изменения?» ConfirmDialog (`labels.discard*`); a swipe past the threshold glides back and asks the same |
|
|
176
178
|
| leaving the tab | `Sheet.Guard busy` or `dirty` | the browser asks before the tab closes or reloads (`beforeunload`), as with `Page.Guard` |
|
|
179
|
+
| title loading | `Sheet.Title loading` | a `Skeleton` holds the title line, `aria-busy` on the `<h2>`; the name fades in under the same element |
|
|
177
180
|
| description loading | `Sheet.Description loading` | a `Skeleton` holds the line, `aria-busy` on the `<p>`; the data fades in under the same element |
|
|
178
181
|
| actions below | the title would get less than 12rem beside `Sheet.Actions` | `data-actions-below` on the header: the actions move under the title and the description |
|
|
182
|
+
| picture mark | an `Avatar.Root` or a `Thumbnail.Root` inside `Sheet.Icon` | `data-media` on the icon, no tile; the picture's `data-size` is the slot's (`l` / `m`) |
|
|
183
|
+
| status in the line | a `Badge.Root` inside `Sheet.Description` | the line is a wrapping flex row, the badges first (`order`), each `data-tier="s"` |
|
|
179
184
|
|
|
180
185
|
Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`overlayMotion`); under `prefers-reduced-motion` it unmounts at once. A swipe down moves the sheet with the pointer; on release it closes past 30% of its height or on a quick flick and glides the rest of the way out, otherwise it glides back — both over `base` on the `emphasized` (iOS sheet) curve. A pull upward meets growing resistance and stops at 24; the sheet's fill continues that far below the screen edge. The scrim thins as the sheet goes down. The drag starts from the handle or the header, never from the body, so its content scrolls and its controls work as usual.
|
|
181
186
|
|
|
@@ -183,6 +188,16 @@ Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`
|
|
|
183
188
|
- A sheet is a short task that ends by itself — a pick, a tap on an action, a swipe down. Its zones follow the task, not a template:
|
|
184
189
|
- **No footer by default.** A footer only for a form with an explicit submit: «Отмена» (`Sheet.Close`, outline neutral) and the submit (solid, last; `type="submit" form={formId}`).
|
|
185
190
|
- **The header is optional.** Keep it when the content needs a title to be understood (a list of actions on «Заказ № 1042»). Drop it — and with it the × and the hairline — when the content speaks for itself (a calendar opened from a «Дата отгрузки» button): name the sheet with `aria-label` on `Sheet.Content`.
|
|
191
|
+
- The header has one anatomy, the same in every modal, drawer and sheet:
|
|
192
|
+
```
|
|
193
|
+
[mark] Title [icon buttons] [⋯] [×]
|
|
194
|
+
[status badges] description text
|
|
195
|
+
```
|
|
196
|
+
- mark — `Sheet.Icon`: a glyph on the tone tile, or the record's picture (`Avatar.Root`, `Thumbnail.Root`, also inside your own wrapper component) with no tile; both are 40 px.
|
|
197
|
+
- line 1 — `Sheet.Title`; `loading` holds it with a Skeleton while the name loads.
|
|
198
|
+
- line 2 — `Sheet.Description`: the record's statuses as `Badge.Root`s first, then the text. The kit sizes the badges (chip tier `s`, the line's 20 px), keeps them first and one gap apart, and wraps the text under them on a narrow panel.
|
|
199
|
+
- end — `Sheet.Actions`: only buttons — ghost icon buttons with a Tooltip and a ⋯ Dropdown — at the close button's tier, centred on the title's line; a status never goes here.
|
|
200
|
+
- A form's action apart from its submit pair: `Sheet.Actions` inside `Sheet.Footer` puts it at the start, at the footer's tier; stacked on a phone it comes first and the primary stays last.
|
|
186
201
|
- The content is the sheet's own, across its width: `Datepicker.Panel` or a column of actions straight in `Sheet.Body` — never a Card or a padded box inside it.
|
|
187
202
|
- Zone padding `--prime-sheet-padding`, width `--prime-sheet-width`. The bottom-sheet surface (`src/internal/sheet.module.css`, shared with a Modal and the floating layers on a phone) gives the height by content up to the top gap, the rounded top, the handle strip and the bottom safe-area inset below the content.
|
|
188
203
|
- Only the body scrolls; header and footer stay in place.
|
|
@@ -232,7 +247,7 @@ Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`
|
|
|
232
247
|
|---|---|
|
|
233
248
|
| [overview.tsx](examples/overview.tsx) | Actions with an order raised from the bottom edge: a header names the order, a column of actions follows, and each action closes the sheet — `Sheet.Trigger`, `Sheet.Header`, `Sheet.Close`. |
|
|
234
249
|
| [structure.tsx](examples/structure.tsx) | The content alone: a date sheet with no header and no footer, the calendar across its width, named by `aria-label`; a pick or a swipe down closes it — `aria-label`, `Sheet.Body`. |
|
|
235
|
-
| [header-actions.tsx](examples/header-actions.tsx) | The courier is at the door: an icon tile heads the sheet, the payment status
|
|
250
|
+
| [header-actions.tsx](examples/header-actions.tsx) | The courier is at the door: an icon tile heads the sheet, the payment status leads the line under the title, a ⋯ menu sits before the close button, and two equal buttons fill the footer — `Sheet.Icon`, `Sheet.Description`, `Sheet.Actions`, `layout`. |
|
|
236
251
|
| [guard.tsx](examples/guard.tsx) | A review form that holds the sheet: after an edit a swipe, the scrim, Escape and Cancel ask before discarding and the footer says so; sending locks every way out — `Sheet.Guard`, `Sheet.Meta`. |
|
|
237
252
|
| [dismiss.tsx](examples/dismiss.tsx) | A handover the courier must answer: no swipe, scrim or Escape closes the sheet, only its two buttons — `closeOnOutsideClick`, `closeOnEscape`. |
|
|
238
253
|
| [controlled-open.tsx](examples/controlled-open.tsx) | The parent owns the state and raises the sheet from a link in the text, without a trigger — `open`, `onOpenChange`. |
|
|
@@ -243,10 +258,14 @@ Motion: the scrim fades, the sheet slides up over `slow` and down over `base` (`
|
|
|
243
258
|
- «Отмена» / «Готово» buttons under a sheet that closes on a pick → drop the footer; the pick, the swipe, the scrim and Escape close it.
|
|
244
259
|
- A header with only a title over a calendar or a list that already says what it is → drop the header and name the sheet with `aria-label`.
|
|
245
260
|
- No header and no `aria-label` → the dialog has no name; give `Sheet.Content` an `aria-label`.
|
|
261
|
+
- A status `Badge` in the header's `Sheet.Actions`, beside the buttons → put it in `Sheet.Description`, before the text: the kit sizes it to the line and keeps it first.
|
|
262
|
+
- A Badge with its own `size` in `Sheet.Description` → leave `size` out: the line gives every header the same chip tier.
|
|
263
|
+
- A `Skeleton` placed by hand in `Sheet.Title` or `Sheet.Description` → their `loading`.
|
|
264
|
+
- A start-side button («Выйти», «Удалить») put inside `Sheet.Meta` → `Sheet.Actions` inside `Sheet.Footer`; `Meta` is status text only.
|
|
246
265
|
- A Card or a padded `div` around the content → put it straight into `Sheet.Body`, across its width.
|
|
247
266
|
- Record details or a long form in a sheet → use Drawer; a sheet is a short task.
|
|
248
267
|
- `closeOnOutsideClick={false}` and `closeOnEscape={false}` with no header × and no `Sheet.Close` → nothing closes it; keep a button that answers.
|
|
249
268
|
|
|
250
269
|
## Related
|
|
251
|
-
- **Built from:** the dialog shared with Modal and Drawer (`internal/overlay/DialogParts`), [Button](../button/COMPONENT.md) (close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Sheet.Body`),
|
|
270
|
+
- **Built from:** the dialog shared with Modal and Drawer (`internal/overlay/DialogParts`), [Button](../button/COMPONENT.md) (close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Sheet.Body`), `internal/mediaSlot` (a record's Avatar or Thumbnail in `Sheet.Icon`), [Skeleton](../skeleton/COMPONENT.md) (`Sheet.Description loading`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Sheet.Guard` discard confirm), `Icon` (the close glyph)
|
|
252
271
|
- **See also:** [Drawer](../drawer/COMPONENT.md) (the same parts as a side panel), [Modal](../modal/COMPONENT.md) (the same parts in the centre), [Datepicker](../datepicker/COMPONENT.md) (`Datepicker.Panel` in a sheet), [Dropdown](../dropdown/COMPONENT.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The courier is at the door: an icon tile heads the sheet, the payment status
|
|
1
|
+
/** The courier is at the door: an icon tile heads the sheet, the payment status leads the line under the title, a ⋯ menu sits before the close button, and two equal buttons fill the footer — `Sheet.Icon`, `Sheet.Description`, `Sheet.Actions`, `layout`. */
|
|
2
2
|
import { Badge, Button, ConfirmDialog, Dropdown, Icon, Sheet, Typography } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
@@ -24,9 +24,11 @@ export default function SheetHeaderActionsExample() {
|
|
|
24
24
|
<Icon name="object.truck" />
|
|
25
25
|
</Sheet.Icon>
|
|
26
26
|
<Sheet.Title>Курьер у двери</Sheet.Title>
|
|
27
|
-
<Sheet.Description
|
|
28
|
-
<Sheet.Actions>
|
|
27
|
+
<Sheet.Description>
|
|
29
28
|
<Badge.Root tone="success">Оплачен</Badge.Root>
|
|
29
|
+
Заказ № 1042 · 3 коробки
|
|
30
|
+
</Sheet.Description>
|
|
31
|
+
<Sheet.Actions>
|
|
30
32
|
<Dropdown.Root>
|
|
31
33
|
<Dropdown.Trigger>
|
|
32
34
|
<Button.Root variant="ghost" tone="neutral" aria-label="Действия с заказом">
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## When to use
|
|
9
9
|
- A region whose content has a known shape is loading: a list, a card, a profile, a form with saved values.
|
|
10
|
-
- The loading state inside [Crossfade](../crossfade/COMPONENT.md): the skeleton holds the data's geometry and
|
|
10
|
+
- The loading state inside [Crossfade](../crossfade/COMPONENT.md) (the state `"loading"`), rendered from the first paint: the skeleton holds the data's geometry and the data replaces it in place with a short fade — the height never glides.
|
|
11
11
|
- First load of a page section, when the structure is known before the values.
|
|
12
12
|
|
|
13
13
|
## When not to use
|
|
@@ -99,7 +99,8 @@ No `labels`.
|
|
|
99
99
|
## Mistakes
|
|
100
100
|
- A hand-drawn grey box with its own `@keyframes` → use `Skeleton`.
|
|
101
101
|
- A spinner in the middle of an empty card while its list loads → skeleton rows of that list.
|
|
102
|
-
- Skeletons of a different geometry than the data (one tall block for three rows) → the swap jumps; mirror the rows.
|
|
102
|
+
- Skeletons of a different geometry than the data (one tall block for three rows) → the swap jumps (it settles in one frame, nothing glides to hide it); mirror the rows.
|
|
103
|
+
- Rendering nothing until the data arrives, or revealing the data by unfolding or sliding it in → the skeleton is there from the first render and the data takes its place.
|
|
103
104
|
- Swapping a skeleton for data without `Crossfade` → wrap the region: `Crossfade state={loading ? "loading" : "ready"}`.
|
|
104
105
|
- Returning to a skeleton on every refresh of data already shown → keep the data, set `aria-busy`.
|
|
105
106
|
|