prime-ui-kit 0.12.0 → 0.13.1
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 +3 -1
- package/dist/chunks/{chunk-YJO3NJIJ.js → chunk-JV2KB5JZ.js} +6662 -5282
- package/dist/chunks/{chunk-VOZ7J72X.js → chunk-PK2GC77E.js} +169 -51
- package/dist/chunks/{chunk-OTOIOHVE.js → chunk-XNOJEX55.js} +14 -5
- package/dist/color-picker.js +2 -2
- package/dist/components/chat/Chat.d.ts +11 -12
- package/dist/components/chat/chatContext.d.ts +2 -0
- package/dist/components/chat-list/ChatList.d.ts +191 -0
- package/dist/components/chat-list/formatChatTime.d.ts +6 -0
- package/dist/components/command-menu/CommandMenu.d.ts +10 -5
- package/dist/components/composer/Composer.d.ts +3 -10
- package/dist/components/data-table/DataTable.d.ts +16 -2
- package/dist/components/data-table/Row.d.ts +23 -7
- package/dist/components/data-table/StateRows.d.ts +5 -1
- package/dist/components/data-table/reorder.d.ts +3 -1
- package/dist/components/data-table/rows.d.ts +20 -4
- package/dist/components/file-upload/FileUpload.d.ts +7 -0
- package/dist/components/gantt/Gantt.d.ts +19 -1
- package/dist/components/gantt/GanttBar.d.ts +4 -1
- package/dist/components/gantt/GanttRow.d.ts +8 -1
- package/dist/components/index.d.ts +10 -8
- package/dist/components/kanban/Kanban.d.ts +17 -2
- package/dist/components/list/List.d.ts +18 -3
- package/dist/components/message/Message.d.ts +13 -0
- package/dist/components/message/MessageParts.d.ts +2 -0
- package/dist/components/modal/DialogParts.d.ts +5 -1
- package/dist/components/select/Select.d.ts +11 -5
- package/dist/components/select/SelectItem.d.ts +19 -0
- package/dist/components/select/selectContext.d.ts +4 -0
- package/dist/components/sparkline/Sparkline.d.ts +14 -3
- package/dist/components/textarea/Textarea.d.ts +11 -3
- package/dist/hooks/useUploadQueue.d.ts +5 -1
- package/dist/icons/registry.d.ts +2 -0
- package/dist/index.css +1 -1
- package/dist/index.js +6 -2
- package/dist/internal/attachmentTile.d.ts +5 -1
- package/dist/internal/richText.d.ts +7 -2
- package/dist/internal/textFormatting.d.ts +52 -0
- package/dist/internal/textFormattingToolbar.d.ts +26 -0
- package/dist/skill/cheatsheet.md +12 -0
- package/dist/skill/choosing.md +5 -2
- package/dist/skill/components.md +5 -4
- package/dist/skill/composition.md +1 -1
- package/dist/skill/layouts.md +2 -0
- package/dist/skill/patterns/chat.module.css +0 -9
- package/dist/skill/patterns/chat.tsx +71 -25
- package/dist/skill/patterns/kanban-board.tsx +2 -8
- package/dist/skill/patterns/schedule.tsx +15 -3
- package/dist/skill/reference/components/chat/COMPONENT.md +13 -4
- package/dist/skill/reference/components/chat/examples/states.tsx +100 -0
- package/dist/skill/reference/components/chat-list/COMPONENT.md +256 -0
- package/dist/skill/reference/components/chat-list/examples/controlled.tsx +103 -0
- package/dist/skill/reference/components/chat-list/examples/examples.module.css +66 -0
- package/dist/skill/reference/components/chat-list/examples/live.tsx +133 -0
- package/dist/skill/reference/components/chat-list/examples/long-list.tsx +87 -0
- package/dist/skill/reference/components/chat-list/examples/narrow.tsx +88 -0
- package/dist/skill/reference/components/chat-list/examples/overview.tsx +194 -0
- package/dist/skill/reference/components/chat-list/examples/search.tsx +71 -0
- package/dist/skill/reference/components/chat-list/examples/sizes.tsx +61 -0
- package/dist/skill/reference/components/chat-list/examples/states.tsx +107 -0
- package/dist/skill/reference/components/chat-list/examples/structure.tsx +135 -0
- package/dist/skill/reference/components/command-menu/COMPONENT.md +15 -11
- package/dist/skill/reference/components/command-menu/examples/server-search.tsx +11 -9
- package/dist/skill/reference/components/composer/COMPONENT.md +8 -7
- package/dist/skill/reference/components/composer/examples/formatting.tsx +1 -1
- package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -0
- package/dist/skill/reference/components/data-table/COMPONENT.md +21 -6
- package/dist/skill/reference/components/data-table/examples/groups.tsx +2 -1
- package/dist/skill/reference/components/data-table/examples/trend.tsx +85 -0
- package/dist/skill/reference/components/drawer/COMPONENT.md +7 -6
- package/dist/skill/reference/components/drawer/examples/client-card.tsx +50 -0
- package/dist/skill/reference/components/fieldset/COMPONENT.md +1 -0
- package/dist/skill/reference/components/file-upload/COMPONENT.md +9 -7
- package/dist/skill/reference/components/file-upload/examples/overview.tsx +14 -3
- package/dist/skill/reference/components/gantt/COMPONENT.md +15 -5
- package/dist/skill/reference/components/gantt/examples/row-link.tsx +96 -0
- package/dist/skill/reference/components/kanban/COMPONENT.md +12 -4
- package/dist/skill/reference/components/kanban/examples/item-link.tsx +92 -0
- package/dist/skill/reference/components/list/COMPONENT.md +19 -4
- package/dist/skill/reference/components/list/examples/reorder.tsx +48 -0
- package/dist/skill/reference/components/message/COMPONENT.md +18 -2
- package/dist/skill/reference/components/message/examples/bot-actions.tsx +51 -0
- package/dist/skill/reference/components/modal/COMPONENT.md +6 -4
- package/dist/skill/reference/components/select/COMPONENT.md +38 -7
- package/dist/skill/reference/components/select/examples/create-action.tsx +107 -0
- package/dist/skill/reference/components/select/examples/saving.tsx +47 -0
- package/dist/skill/reference/components/sheet/COMPONENT.md +4 -4
- package/dist/skill/reference/components/sparkline/COMPONENT.md +16 -2
- package/dist/skill/reference/components/sparkline/examples/compact.tsx +41 -0
- package/dist/skill/reference/components/sparkline/examples/examples.module.css +12 -0
- package/dist/skill/reference/components/textarea/COMPONENT.md +22 -2
- package/dist/skill/reference/components/textarea/examples/formatting.tsx +20 -0
- package/package.json +2 -2
- package/src/components/chat/COMPONENT.md +13 -4
- package/src/components/chat/examples/states.tsx +100 -0
- package/src/components/chat-list/COMPONENT.md +256 -0
- package/src/components/chat-list/examples/controlled.tsx +103 -0
- package/src/components/chat-list/examples/examples.module.css +66 -0
- package/src/components/chat-list/examples/live.tsx +133 -0
- package/src/components/chat-list/examples/long-list.tsx +87 -0
- package/src/components/chat-list/examples/narrow.tsx +88 -0
- package/src/components/chat-list/examples/overview.tsx +194 -0
- package/src/components/chat-list/examples/search.tsx +71 -0
- package/src/components/chat-list/examples/sizes.tsx +61 -0
- package/src/components/chat-list/examples/states.tsx +107 -0
- package/src/components/chat-list/examples/structure.tsx +135 -0
- package/src/components/command-menu/COMPONENT.md +15 -11
- package/src/components/command-menu/examples/server-search.tsx +11 -9
- package/src/components/composer/COMPONENT.md +8 -7
- package/src/components/composer/examples/formatting.tsx +1 -1
- package/src/components/confirm-dialog/COMPONENT.md +1 -0
- package/src/components/data-table/COMPONENT.md +21 -6
- package/src/components/data-table/examples/groups.tsx +2 -1
- package/src/components/data-table/examples/trend.tsx +85 -0
- package/src/components/drawer/COMPONENT.md +7 -6
- package/src/components/drawer/examples/client-card.tsx +50 -0
- package/src/components/fieldset/COMPONENT.md +1 -0
- package/src/components/file-upload/COMPONENT.md +9 -7
- package/src/components/file-upload/examples/overview.tsx +14 -3
- package/src/components/gantt/COMPONENT.md +15 -5
- package/src/components/gantt/examples/row-link.tsx +96 -0
- package/src/components/kanban/COMPONENT.md +12 -4
- package/src/components/kanban/examples/item-link.tsx +92 -0
- package/src/components/list/COMPONENT.md +19 -4
- package/src/components/list/examples/reorder.tsx +48 -0
- package/src/components/message/COMPONENT.md +18 -2
- package/src/components/message/examples/bot-actions.tsx +51 -0
- package/src/components/modal/COMPONENT.md +6 -4
- package/src/components/select/COMPONENT.md +38 -7
- package/src/components/select/examples/create-action.tsx +107 -0
- package/src/components/select/examples/saving.tsx +47 -0
- package/src/components/sheet/COMPONENT.md +4 -4
- package/src/components/sparkline/COMPONENT.md +16 -2
- package/src/components/sparkline/examples/compact.tsx +41 -0
- package/src/components/sparkline/examples/examples.module.css +12 -0
- package/src/components/textarea/COMPONENT.md +22 -2
- package/src/components/textarea/examples/formatting.tsx +20 -0
- package/dist/components/composer/format.d.ts +0 -39
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Card,
|
|
12
12
|
ChartFrame,
|
|
13
13
|
Chat,
|
|
14
|
+
ChatList,
|
|
14
15
|
CodeBlock,
|
|
15
16
|
ColorPresets,
|
|
16
17
|
CommandMenu,
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
celebrate,
|
|
68
69
|
chartColors,
|
|
69
70
|
datepickerPresets,
|
|
71
|
+
formatChatTime,
|
|
70
72
|
formatDatepickerValue,
|
|
71
73
|
formatFileSize,
|
|
72
74
|
formatMessageDate,
|
|
@@ -83,7 +85,7 @@ import {
|
|
|
83
85
|
useSidebar,
|
|
84
86
|
useTokenValue,
|
|
85
87
|
useUploadQueue
|
|
86
|
-
} from "./chunks/chunk-
|
|
88
|
+
} from "./chunks/chunk-JV2KB5JZ.js";
|
|
87
89
|
import {
|
|
88
90
|
Avatar,
|
|
89
91
|
Badge,
|
|
@@ -115,7 +117,7 @@ import {
|
|
|
115
117
|
textTones,
|
|
116
118
|
tones,
|
|
117
119
|
variants
|
|
118
|
-
} from "./chunks/chunk-
|
|
120
|
+
} from "./chunks/chunk-PK2GC77E.js";
|
|
119
121
|
import {
|
|
120
122
|
ControlSizeProvider,
|
|
121
123
|
useOptionalControlSize
|
|
@@ -135,6 +137,7 @@ export {
|
|
|
135
137
|
Card,
|
|
136
138
|
ChartFrame,
|
|
137
139
|
Chat,
|
|
140
|
+
ChatList,
|
|
138
141
|
Checkbox,
|
|
139
142
|
CodeBlock,
|
|
140
143
|
ColorPresets,
|
|
@@ -212,6 +215,7 @@ export {
|
|
|
212
215
|
createIcon,
|
|
213
216
|
dataStates,
|
|
214
217
|
datepickerPresets,
|
|
218
|
+
formatChatTime,
|
|
215
219
|
formatDatepickerValue,
|
|
216
220
|
formatFileSize,
|
|
217
221
|
formatMessageDate,
|
|
@@ -16,6 +16,10 @@ export type AttachmentTileProps = Omit<React.HTMLAttributes<HTMLElement>, "child
|
|
|
16
16
|
nameTitle?: string;
|
|
17
17
|
/** The size, at the end of the bottom band; a video without it shows its duration. */
|
|
18
18
|
meta?: string;
|
|
19
|
+
/** The file's address: the tile's middle becomes a link that opens it in a new tab. */
|
|
20
|
+
href?: string;
|
|
21
|
+
/** Name of the `href` link («Открыть договор.pdf»). */
|
|
22
|
+
openLabel?: string;
|
|
19
23
|
/**
|
|
20
24
|
* The pill at the top start — the format (JPG, PDF) or a word such as «Обложка»; decorative, the
|
|
21
25
|
* name says it. On a picture or a video it stands on the media shade; a file passes a kit Badge.
|
|
@@ -56,7 +60,7 @@ export type AttachmentTileProps = Omit<React.HTMLAttributes<HTMLElement>, "child
|
|
|
56
60
|
* picture, a video's first frame (lazy) with its play mark, or the sheet of a file; the upload disk
|
|
57
61
|
* with its progress ring stands over the middle. Shared by FileUpload tiles and the Composer tray.
|
|
58
62
|
*/
|
|
59
|
-
export declare function AttachmentTile({ id, kind, src, name, nameTitle, meta, badge, upload, uploadState, cancelLabel, retryLabel, onRetry, onRemove, removeLabel, pending, arrived, leaving, className, ref, ...rest }: AttachmentTileProps): import("react/jsx-runtime").JSX.Element;
|
|
63
|
+
export declare function AttachmentTile({ id, kind, src, name, nameTitle, meta, href, openLabel, badge, upload, uploadState, cancelLabel, retryLabel, onRetry, onRemove, removeLabel, pending, arrived, leaving, className, ref, ...rest }: AttachmentTileProps): import("react/jsx-runtime").JSX.Element;
|
|
60
64
|
/** A row of tiles scrolls sideways — its hidden edges fade, a scroll rests on a tile, a vertical
|
|
61
65
|
* wheel moves it — with the bar hidden. */
|
|
62
66
|
export declare function AttachmentStripScroller({ children }: {
|
|
@@ -3,7 +3,7 @@ export type RichInline = {
|
|
|
3
3
|
type: "text";
|
|
4
4
|
text: string;
|
|
5
5
|
} | {
|
|
6
|
-
type: "bold" | "italic" | "strike";
|
|
6
|
+
type: "bold" | "italic" | "strike" | "spoiler";
|
|
7
7
|
children: RichInline[];
|
|
8
8
|
} | {
|
|
9
9
|
type: "code";
|
|
@@ -29,12 +29,17 @@ export declare function safeHref(raw: string): string | null;
|
|
|
29
29
|
export declare function parseInline(source: string): RichInline[];
|
|
30
30
|
/** Blocks of a message: runs of text, quotes (`> ` lines) and fenced code. */
|
|
31
31
|
export declare function parseRichText(source: string): RichBlock[];
|
|
32
|
+
export type RichTextLabels = {
|
|
33
|
+
/** Accessible name of a masked spoiler: its text is not read until it is revealed. */
|
|
34
|
+
spoiler: string;
|
|
35
|
+
};
|
|
32
36
|
export type RichTextProps = {
|
|
33
37
|
source: string;
|
|
34
38
|
/** Rendered at the very end of the last line (a chat bubble's meta reserve). */
|
|
35
39
|
end?: React.ReactNode;
|
|
36
40
|
/** Marks every case-insensitive match of this text (a search through a chat). */
|
|
37
41
|
highlight?: string;
|
|
42
|
+
labels?: Partial<RichTextLabels>;
|
|
38
43
|
};
|
|
39
44
|
/** A message text with its Markdown subset rendered safely; `end` closes the last text line. */
|
|
40
|
-
export declare function RichText({ source, end, highlight }: RichTextProps): import("react/jsx-runtime").JSX.Element;
|
|
45
|
+
export declare function RichText({ source, end, highlight, labels: labelsProp }: RichTextProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown formatting of a plain text (the formatting toolbar and hotkeys of Composer and Textarea,
|
|
3
|
+
* `useTextFormatting`): pure functions over the text and its selection, so a component only applies
|
|
4
|
+
* the result to its textarea. `internal/richText` renders the same subset.
|
|
5
|
+
*/
|
|
6
|
+
/** A formatting command: what the toolbar button or the hotkey applies to the selection. */
|
|
7
|
+
export type TextFormat = "bold" | "italic" | "strikethrough" | "code" | "spoiler" | "link" | "quote";
|
|
8
|
+
/** Names of the formatting toolbar and of its tools, one per format. */
|
|
9
|
+
export type TextFormattingLabels = Record<TextFormat, string> & {
|
|
10
|
+
/** Accessible name of the formatting toolbar. */
|
|
11
|
+
formatting: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const TEXT_FORMATTING_LABELS: TextFormattingLabels;
|
|
14
|
+
/** Every format in toolbar order. */
|
|
15
|
+
export declare const TEXT_FORMATS: readonly TextFormat[];
|
|
16
|
+
/** Text with its selection, `start <= end` (a caret when equal). */
|
|
17
|
+
export type TextSelection = {
|
|
18
|
+
text: string;
|
|
19
|
+
start: number;
|
|
20
|
+
end: number;
|
|
21
|
+
};
|
|
22
|
+
/** The text and selection after `format` is applied to the selection. */
|
|
23
|
+
export declare function applyFormat(selection: TextSelection, format: TextFormat): TextSelection;
|
|
24
|
+
/**
|
|
25
|
+
* The one span that differs between two texts (common prefix and suffix removed), so the change can
|
|
26
|
+
* be typed into the textarea as one edit and stay on its undo stack.
|
|
27
|
+
*/
|
|
28
|
+
export declare function diffRange(before: string, after: string): {
|
|
29
|
+
start: number;
|
|
30
|
+
end: number;
|
|
31
|
+
insert: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The format a key press asks for: Mod+B · Mod+I · Mod+Shift+X · Mod+E · Mod+Shift+H · Mod+K ·
|
|
35
|
+
* Mod+Shift+9. The spoiler is not Telegram's Mod+Shift+P: Firefox and Edge reserve it for a private
|
|
36
|
+
* window and never hand it to the page.
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatForKey(event: {
|
|
39
|
+
key: string;
|
|
40
|
+
code: string;
|
|
41
|
+
metaKey: boolean;
|
|
42
|
+
ctrlKey: boolean;
|
|
43
|
+
shiftKey: boolean;
|
|
44
|
+
altKey: boolean;
|
|
45
|
+
}): TextFormat | null;
|
|
46
|
+
/** Shortcut of every format, by modifier names: rendered as `⌘B` on Apple systems, `Ctrl+B` elsewhere. */
|
|
47
|
+
export declare const FORMAT_SHORTCUTS: Record<TextFormat, {
|
|
48
|
+
shift: boolean;
|
|
49
|
+
key: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** `⌘⇧X` on Apple systems, `Ctrl+Shift+X` elsewhere. */
|
|
52
|
+
export declare function shortcutText(format: TextFormat, apple: boolean): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type ControlSize } from "./states";
|
|
3
|
+
import { type TextFormattingLabels } from "./textFormatting";
|
|
4
|
+
export type TextFormattingOptions = {
|
|
5
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
6
|
+
/** The hotkeys and the toolbar. */
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
/** Whether the toolbar may show now (not while the field is disabled, read-only or covered). */
|
|
9
|
+
toolbar: boolean;
|
|
10
|
+
labels: TextFormattingLabels;
|
|
11
|
+
/** Tier of the field; the tools stand one tier smaller. */
|
|
12
|
+
size: ControlSize;
|
|
13
|
+
/** Id of the textarea (`aria-controls` of the toolbar). */
|
|
14
|
+
controlsId?: string;
|
|
15
|
+
};
|
|
16
|
+
export type TextFormatting = {
|
|
17
|
+
/** On the textarea's `onSelect`. */
|
|
18
|
+
onSelect: () => void;
|
|
19
|
+
/** Call first in the textarea's `onKeyDown`; `true` when the key was a formatting one and is used. */
|
|
20
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => boolean;
|
|
21
|
+
/** On the `position: relative` box around the field and the toolbar. */
|
|
22
|
+
anchorProps: Pick<React.HTMLAttributes<HTMLElement>, "onFocus" | "onBlur">;
|
|
23
|
+
/** The toolbar while it shows or leaves. */
|
|
24
|
+
toolbar: React.ReactNode;
|
|
25
|
+
};
|
|
26
|
+
export declare function useTextFormatting({ textareaRef, enabled, toolbar: toolbarAllowed, labels, size, controlsId, }: TextFormattingOptions): TextFormatting;
|
package/dist/skill/cheatsheet.md
CHANGED
|
@@ -34,8 +34,10 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
|
|
|
34
34
|
- Show `error` after blur or on submit; `<form noValidate onSubmit>` + `<Button.Root type="submit" loading>` — not validation on every keystroke.
|
|
35
35
|
- Groups of fields: `<Fieldset label="Контакты" disabled={saving}>` with the submit `<Button.Root loading>` outside it — not a hand-reset `<fieldset>` with a `Typography` heading, not `disabled` on every field while saving. One choice with a heading (`Radio.Group label`) needs no Fieldset.
|
|
36
36
|
- Uploads: `const queue = useUploadQueue({ upload })` → `<FileUpload.Root queue={queue} />` (`FileUpload.Queue` when the picker lives elsewhere) — progress, «В очереди», the reason of a failure, retry and remove come with it (three at once by default, `concurrency`); not a hand-kept list of files with own progress bars. A photo appears at once from its local preview.
|
|
37
|
+
- After-upload work (attach the file to a record) runs inside `upload` after the request resolves — `context.id` is the item; a file the user can open is `FileUploadFile.href`, not your own download button beside the row.
|
|
37
38
|
- A choice in a form (SegmentedControl, ButtonGroup) is a field too: `label`, `hint`, `error` on its root — not a `Label` wired by hand.
|
|
38
39
|
- A record card that saves each field: `<Input.Root onCommit={save}>` (save returns a Promise) — not hand-written blur / Enter / Escape handlers or a field disabled while saving.
|
|
40
|
+
- Markdown text outside a chat (an announcement, a task description) is `<Textarea.Root formatting>`: the Composer's toolbar, hotkeys and subset (`**`, `_`, `~~`, `` ` ``, `||spoiler||`, links, `> `) — not a hand-made toolbar over a textarea. Turn it on only where the text is shown with its Markdown.
|
|
39
41
|
- Copy or call next to a value: `Input.CopyButton`, `Input.Action` inside `Input.Wrapper` — not a Button beside the field; a password: `Input.RevealButton`.
|
|
40
42
|
|
|
41
43
|
## State
|
|
@@ -44,6 +46,9 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
|
|
|
44
46
|
- Controlled triads, same names everywhere: `value` / `onValueChange`, `checked` / `onCheckedChange`, `open` / `onOpenChange` — not `onChange(event)` (except the native `Input.Field` / Textarea `onChange`), `isOpen`, `onToggle`.
|
|
45
47
|
- Selection count is `multiple` (`Select.Root multiple`) — not `type="multiple"`.
|
|
46
48
|
- A slow or risky action is an async event: return the request's Promise from `onConfirm`, `onCommit`, `onReorder`, `onBarChange`, `onSend`, a Switch's `onCheckedChange`, a Slider's `onCommit`, a DigitInput's `onComplete`, a Kanban or FileUpload change, a Badge `onRemove`, TagSelect `onCreate`… — the component shows pending, keeps the change on resolve and rolls it back on reject; not `true` / `false`, not your own pending flag, no `onBeforeX`.
|
|
49
|
+
- A Select that saves on pick: return the save's Promise from `onValueChange` — the value shows at once with a Spinner on the trigger and a rejection rolls it back (`labels.changeFailed`); never reset the value by hand. A «Новый клиент» row in the list is `Select.Action` (`onSelect`), never a fake `Select.Item` with a sentinel value or a button under the field.
|
|
50
|
+
- An option reads its parts by element type: put `Avatar.Root`, `Thumbnail.Root`, `Badge.Root` and `Select.Item*` straight in the item and call your helper as a function (`{clientAvatar(c)}`), not as `<ClientAvatar />`; anything else trailing goes in `Select.ItemMeta`.
|
|
51
|
+
- Create from a search: one `persistent` `CommandMenu.Item` at the end («Создать клиента «{query}»», while the query is not empty) — it stays when a namesake matches; `CommandMenu.Empty` holds only the text.
|
|
47
52
|
- A value that settles at the end of a gesture saves in its own event: Slider `onCommit` — not a request on every `onValueChange` step.
|
|
48
53
|
|
|
49
54
|
## Text: labels vs children
|
|
@@ -87,12 +92,18 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
|
|
|
87
92
|
- A chart with axes or several series: `<ChartFrame.Root series state>` around the library (or your SVG) with `ChartFrame.Plot renderPlot`, `Legend`, `Table` — not a hand-made Card with a grey box while loading and a legend of colored dots.
|
|
88
93
|
- Amounts, counts and dates are `numeric` columns; statuses are `Badge.Root color` from one status → color map.
|
|
89
94
|
- Rows people order by hand: `onReorder(id, beforeId)` (apply it with `moveBefore`, return the save's Promise) — not a Dnd list rebuilt next to the table.
|
|
95
|
+
- A short list of records people order by hand (stages, priorities) is `<List.Root onReorder>` with `List.Item value` — not up / down buttons or a hand-made `Dnd.Sortable`.
|
|
96
|
+
- Groups that fold: pass `defaultCollapsedGroups={[]}` (or the controlled `collapsedGroups` + `onCollapsedGroupsChange`) — not your own button in `renderGroupHeader` or filtering `rows`. One heatmap over several columns: the same `scale: { min, max }` on each.
|
|
97
|
+
- A trend in a table row is `<Sparkline layout="compact">` in the column's `cell` (a column `width`, series built once, not inline); the number itself goes in its own column.
|
|
90
98
|
- Properties of one record: `<DescriptionList.Root>` of `DescriptionList.Item` — not a hand-made grid of label / value text, not a one-row DataTable.
|
|
91
99
|
- Who is busy when, a plan with dates: `Gantt` with `onBarChange` / `onBarCreate` — not bars drawn by hand or a DataTable of start and end dates. Statuses moved by hand: `Kanban`.
|
|
100
|
+
- Open a board card with `Kanban.Root onItemClick` (click, Enter, Space, never after a drag) or `getItemHref`; a Gantt row with `onRowClick` / `getRowHref` (the DataTable contract), a bar with `onSelect`; more facts in a bar tooltip with `renderBarTooltip` (it adds lines under the dates). A legend of hues is a row of `Badge.Root color` + `Badge.Dot` in the host's `toolbar`, the category also in text. Hour slots (bookings, appointments) are not Gantt — its unit is a calendar day.
|
|
92
101
|
|
|
93
102
|
## Chat
|
|
94
103
|
|
|
95
104
|
- A conversation is `Chat`: `<Chat.Root typing selected search>` with `Chat.Header` (avatar, `Chat.Title`, `Chat.Description`, `Chat.Actions` + `Chat.SearchButton`), `Chat.Search`, `Chat.Pinned`, `<Chat.Messages items getKey getAuthor getDate renderMessage>` and `<Chat.Footer><Composer.Root /></Chat.Footer>` — not a header, a ScrollContainer and a list of your own, not Cards or a Timeline styled as bubbles, not a Textarea with a send button. A list without the rest is `Chat.Root` with `Chat.Messages` alone; one message on its own is `Message.Root`.
|
|
105
|
+
- The list of conversations is `ChatList` — not a `List` or Cards with your own preview, unread counts and «печатает…». Back button and avatar in the chat header are plain children of `Chat.Header`: it lays them out side by side.
|
|
106
|
+
- A dialog loading is `<Chat.Messages loading>` (bubble skeletons that cross-fade into the list) — not a Spinner or grey boxes. A bot's buttons under a message are `Message.Actions` rows of `Button.Root variant="soft" tone="neutral"` (`asChild` for links), never buttons inside `Message.Text`.
|
|
96
107
|
- `renderMessage` returns a `Message.Root` (its `value` and `position` are set for you) or a `Chat.Service`; keep it stable with `useCallback` (other messages it looks up through a ref) — an item renders again only when it, its place or the function changes. Dates, runs and the unread band (`unread={key}`) come from the list.
|
|
97
108
|
- A group chat is `<Chat.Messages type="group">`: put `Message.Author` (`color={hueOf(user.id)}`, `tag`, `Message.AuthorIcon` for a crown) and `Message.Avatar` on every message — the list shows the header on the first of a run and the avatar on the last; a direct chat shows neither.
|
|
98
109
|
- A voice message is `VoicePlayer` inside `Message.Root`; a round video is `VideoNote` inside `Message.Root`; photos, files, places and contacts are `Message` parts — not FileUpload rows or Cards. The same album outside a chat is `Gallery`. The two players share one API: learn one, use both.
|
|
@@ -111,6 +122,7 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
|
|
|
111
122
|
## Overlays
|
|
112
123
|
|
|
113
124
|
- Blocking decision or ≤ 4 fields → `Modal` (`<Modal.Content size="s">` for a confirm); long form, filters, details → `Drawer`; a short task that ends by itself (a date, an action, a note) → `Sheet`; small panel at a control → `Popover`; list of actions → `Dropdown`; a field value → `Select`; a name for an icon button → `Tooltip`; ⌘K → `CommandMenu`; a photo, a scan or a video full screen from its thumbnail → `MediaViewer` (an album in a grid is `Gallery`, which opens it for you; a standalone viewer keeps `open` and `index` in two states) — not a Modal with an `<img>`.
|
|
125
|
+
- A record's drawer or modal shows its person or object as `Drawer.Icon` / `Modal.Icon` around `Avatar.Root` / `Thumbnail.Root`. A confirm that asks for input (a reason) is a `Modal size="s"` with the field, `Modal.Confirm` and `Modal.Guard busy` — `ConfirmDialog` is a question only.
|
|
114
126
|
- Never a hand-made `div` backdrop, dialog, menu or tooltip.
|
|
115
127
|
- Every Modal and Drawer has all three zones: `Header` (title, ×) · `Body` (content, scrolls) · `Footer` (actions). Details: `<Drawer.Footer><Drawer.Close><Button.Root variant="outline" tone="neutral">Закрыть</Button.Root></Drawer.Close></Drawer.Footer>`, plus the main action last; a form: «Отмена» + submit. Not a drawer that ends with the body, not Save at the bottom of the body. A `Sheet` (from the bottom edge) is the opposite: no footer by default — a pick or a swipe ends it — and its header is optional: keep it when the content needs a title, drop it (× and hairline go too) when the content speaks for itself, with `aria-label` on `Sheet.Content`. Never `Drawer.Content side="bottom"`.
|
|
116
128
|
- Content inside a Drawer, a Sheet or a Card is the host's own: a calendar is `Datepicker.Panel` straight in the body, across its width — no card, no padded box around it.
|
package/dist/skill/choosing.md
CHANGED
|
@@ -11,7 +11,7 @@ confuse. Links: [components.md](components.md). Whole screens: [composition.md](
|
|
|
11
11
|
| type a value (text, number, code, file, photos), group the fields of a form | Inputs — Input, Textarea, DigitInput, FileUpload, Label, Hint, Fieldset |
|
|
12
12
|
| pick from options, toggle, set a range, filter, a date or a color | Selection — Checkbox, Radio, Switch, SegmentedControl, Slider, Select, NativeSelect, TagSelect, SmartFilter, Datepicker, ColorField, ColorPicker, ColorSwatches |
|
|
13
13
|
| see data: status labels, people, objects, numbers and their trend, properties of a record, rows, a board, a schedule, events, code, disclosed sections | Data display — Badge, Avatar, Thumbnail, Kbd, Card, DescriptionList, DataTable, Kanban, Gantt, Timeline, CodeBlock, Accordion, Sparkline, ChartFrame |
|
|
14
|
-
| hold a conversation: read and send chat messages of every kind | Messages — Chat, Message, Gallery, VoicePlayer, VideoNote, Composer |
|
|
14
|
+
| hold a conversation: pick a dialog, read and send chat messages of every kind | Messages — Chat, ChatList, Message, Gallery, VoicePlayer, VideoNote, Composer |
|
|
15
15
|
| learn what happened, how far along it is, or watch a region load and change state; say it to a screen reader | Status and loading — Banner, Notification, ProgressBar, ProgressCircle, Spinner, Skeleton, Crossfade, EmptyPage, VisuallyHidden |
|
|
16
16
|
| move between places: app sections, panels of a screen, the records of a list, the path, pages, steps | Navigation — Tabs, Breadcrumb, Pagination, List, Stepper, Sidebar, BottomNav |
|
|
17
17
|
| see something on top of the page, or a photo or video full screen | Overlays — Tooltip, Popover, Dropdown, Modal, ConfirmDialog, Drawer, Sheet, CommandMenu, MediaViewer |
|
|
@@ -74,7 +74,10 @@ never a Timeline or a list of Cards styled as bubbles. Code → CodeBlock. A num
|
|
|
74
74
|
`Chat.Messages` (from `items` with `renderMessage`, or `Message.Root` children) and `Chat.Footer`
|
|
75
75
|
with the Composer — never a hand-built header, ScrollContainer and list. A list of messages without
|
|
76
76
|
the rest (comments as a chat, a read-only history) → `Chat.Root` with `Chat.Messages` alone; one
|
|
77
|
-
message on its own → `Message.Root`.
|
|
77
|
+
message on its own → `Message.Root`. The list of conversations beside the chat (the dialogs pane of a
|
|
78
|
+
messenger or an inbox) → `ChatList` (`items`, `renderItem` with `ChatList.Item`, `value`): unread
|
|
79
|
+
counts, «печатает…», drafts, ticks, a row menu and search come with it — never `List` with a hand-made
|
|
80
|
+
count and preview; `List` stays for records that are not conversations (tickets, letters, invoices). A group chat → `Chat.Messages type="group"`: `Message.Author`
|
|
78
81
|
(`tag`, `Message.AuthorIcon`) and `Message.Avatar` on every message, the list shows them on the
|
|
79
82
|
first and the last of a run. A voice message → `VoicePlayer` inside `Message.Root`; a call recording
|
|
80
83
|
outside a chat → `VoicePlayer` alone. A round video message → `VideoNote` inside `Message.Root`
|
package/dist/skill/components.md
CHANGED
|
@@ -24,7 +24,7 @@ Type a value, and the field anatomy: label, hint, error.
|
|
|
24
24
|
| Component | Purpose | Docs |
|
|
25
25
|
|---|---|---|
|
|
26
26
|
| Input | Single-line text field with label, hint, error and slots for icons, affixes, a clear button and a counter. | [COMPONENT.md](reference/components/input/COMPONENT.md) · [examples](reference/components/input/examples/) |
|
|
27
|
-
| Textarea | Multi-line text field with label, hint, error and a character counter; grows with its content from `rows` lines up to `maxRows`, then scrolls inside. | [COMPONENT.md](reference/components/textarea/COMPONENT.md) · [examples](reference/components/textarea/examples/) |
|
|
27
|
+
| Textarea | Multi-line text field with label, hint, error and a character counter; grows with its content from `rows` lines up to `maxRows`, then scrolls inside; `formatting` adds the Composer's Markdown toolbar and hotkeys. | [COMPONENT.md](reference/components/textarea/COMPONENT.md) · [examples](reference/components/textarea/examples/) |
|
|
28
28
|
| DigitInput | A row of square single-digit cells for a fixed-length code (OTP from SMS, PIN, pickup code), with the field label, hint and error. | [COMPONENT.md](reference/components/digit-input/COMPONENT.md) · [examples](reference/components/digit-input/examples/) |
|
|
29
29
|
| FileUpload | Files and the image gallery in one component: a document field (rows), file cards, an image gallery with a cover (`trigger="tile"` `layout="grid"` `cover`) or an attachment tray (`trigger="button"` + `FileUpload.Queue layout="strip"`); uploads through a queue (`useUploadQueue`), reorder and server saves. | [COMPONENT.md](reference/components/file-upload/COMPONENT.md) · [examples](reference/components/file-upload/examples/) |
|
|
30
30
|
| Label | Field label (native `<label>`) with required and optional markers. | [COMPONENT.md](reference/components/label/COMPONENT.md) · [examples](reference/components/label/examples/) |
|
|
@@ -60,7 +60,7 @@ Show data: labels, people, objects, cards, tables, boards, feeds, code, disclose
|
|
|
60
60
|
| Badge | The kit's one chip: a static label for a status, category or count, a removable value or applied filter, and a pressable toggle with a hover action — in a palette color. | [COMPONENT.md](reference/components/badge/COMPONENT.md) · [examples](reference/components/badge/examples/) |
|
|
61
61
|
| Avatar | A round photo of a person or organization with an initials or icon fallback, a presence dot and overlapping groups. | [COMPONENT.md](reference/components/avatar/COMPONENT.md) · [examples](reference/components/avatar/examples/) |
|
|
62
62
|
| Thumbnail | A preview of an object — product, vehicle, file, cover — at a fixed aspect ratio (`ratio`), an image or a video (`Thumbnail.Video`, first frame and length), a colored icon fallback, and a selectable tile (`checked`). | [COMPONENT.md](reference/components/thumbnail/COMPONENT.md) · [examples](reference/components/thumbnail/examples/) |
|
|
63
|
-
| Sparkline | A small line or bar chart with its headline — the value at rest, its change against the point before and its date — where any point can be scrubbed. | [COMPONENT.md](reference/components/sparkline/COMPONENT.md) · [examples](reference/components/sparkline/examples/) |
|
|
63
|
+
| Sparkline | A small line or bar chart with its headline — the value at rest, its change against the point before and its date — where any point can be scrubbed; `layout="compact"` — just the plot, for a table cell. | [COMPONENT.md](reference/components/sparkline/COMPONENT.md) · [examples](reference/components/sparkline/examples/) |
|
|
64
64
|
| ChartFrame | A dashboard panel around any chart (Recharts, ECharts, visx, Chart.js or a hand-drawn SVG): title, headline value, a measured plot area, a series legend that hides series (`hiddenSeries`), loading / empty / error states (`state`, `onRetry`) that cross-fade, a tooltip body and a data table for screen readers. | [COMPONENT.md](reference/components/chart-frame/COMPONENT.md) · [examples](reference/components/chart-frame/examples/) |
|
|
65
65
|
| Kbd | A key cap for a keyboard key or a shortcut, rendered as a native `<kbd>`. | [COMPONENT.md](reference/components/kbd/COMPONENT.md) · [examples](reference/components/kbd/examples/) |
|
|
66
66
|
| Card | A filled surface block with structural templates for metrics, charts, lists, calls to action and covers. | [COMPONENT.md](reference/components/card/COMPONENT.md) · [examples](reference/components/card/examples/) |
|
|
@@ -78,12 +78,13 @@ A conversation: message bubbles of every kind and the composer.
|
|
|
78
78
|
|
|
79
79
|
| Component | Purpose | Docs |
|
|
80
80
|
|---|---|---|
|
|
81
|
-
| Chat | A conversation assembled: `Chat.Header` (avatar, title, status that turns into «печатает…», actions, `Chat.SearchButton`), `Chat.Search` (marks, counts and steps through matches), `Chat.Pinned`, `Chat.Messages` — the one message list: from `items` + `renderMessage` (dates, runs, unread band added) or `Message.Root` children, virtualized, new messages that enter by themselves, the «к последним» button, read receipts, jumps, selection with `Chat.SelectionBar`, history paging, `type="group"` author headers and avatars — and `Chat.Footer` for the Composer. | [COMPONENT.md](reference/components/chat/COMPONENT.md) · [examples](reference/components/chat/examples/) |
|
|
82
|
-
| Message | One chat message in its shell (bubble, picture, circle): incoming / outgoing, runs of one author (the tail ends a run), time and delivery ticks in the corner, a group chat's author header (`Message.Author` with `tag` and `Message.AuthorIcon`), reply, forwarded, reactions with the reaction bar, the context menu, and every content kind (text with Markdown, `Message.Media` albums, round video, voice, file, location, contact, link preview); `Message.Typing
|
|
81
|
+
| Chat | A conversation assembled: `Chat.Header` (avatar, title, status that turns into «печатает…», actions, `Chat.SearchButton`), `Chat.Search` (marks, counts and steps through matches), `Chat.Pinned`, `Chat.Messages` — the one message list: from `items` + `renderMessage` (dates, runs, unread band added) or `Message.Root` children, virtualized, new messages that enter by themselves, the «к последним» button, read receipts, jumps, selection with `Chat.SelectionBar`, history paging, `type="group"` author headers and avatars — and `Chat.Footer` for the Composer; a first-load state of bubble skeletons (`Chat.Messages loading`). | [COMPONENT.md](reference/components/chat/COMPONENT.md) · [examples](reference/components/chat/examples/) |
|
|
82
|
+
| Message | One chat message in its shell (bubble, picture, circle): incoming / outgoing, runs of one author (the tail ends a run), time and delivery ticks in the corner, a group chat's author header (`Message.Author` with `tag` and `Message.AuthorIcon`), reply, forwarded, reactions with the reaction bar, the context menu, and every content kind (text with Markdown, `Message.Media` albums, round video, voice, file, location, contact, link preview); `Message.Typing`; a bot's inline keyboard (`Message.Actions`); spoilers in the Markdown. | [COMPONENT.md](reference/components/message/COMPONENT.md) · [examples](reference/components/message/examples/) |
|
|
83
83
|
| Gallery | Photos and videos as a messenger album: one, or up to ten laid out from their aspect ratios with nothing measured; video tiles with a first frame and length, the shared transfer disk per tile, tiles that open the album in MediaViewer (`viewer={false}` to open your own from `onOpenChange` / `onIndexChange`). `Message.Media` is a Gallery in a message. | [COMPONENT.md](reference/components/gallery/COMPONENT.md) · [examples](reference/components/gallery/examples/) |
|
|
84
84
|
| VoicePlayer | A voice message player: a round play disk, a waveform that is the seek slider, and the time with the playback speed — in a chat bubble or standalone; one sound at a time on the page; `getWaveform` for audio without levels. | [COMPONENT.md](reference/components/voice-player/COMPONENT.md) · [examples](reference/components/voice-player/examples/) |
|
|
85
85
|
| VideoNote | A round video message: a still first frame with the play disk, played with sound on a press; the same player API as VoicePlayer (transcript, transfer either way, one sound at a time); alone in `Message.Root` it is the message. | [COMPONENT.md](reference/components/video-note/COMPONENT.md) · [examples](reference/components/video-note/examples/) |
|
|
86
86
|
| Composer | The message input of a chat: a field that grows from one line, Enter to send, attachments through an upload queue, a reply / edit bar whose id goes with the send, Markdown formatting of a selection, voice and round video messages with pause, and the «печатает…» signal (`onActivityChange`). | [COMPONENT.md](reference/components/composer/COMPONENT.md) · [examples](reference/components/composer/examples/) |
|
|
87
|
+
| ChatList | The conversation list of a messenger, built from List: dialogs from `items` + `renderItem` (`ChatList.Item` with the avatar, name, time, preview), the open one `value`, a search field that marks matches, unread / muted / «@» counts, pin, delivery ticks, «печатает…» and drafts that cross-fade, a row menu at the pointer (`ChatList.ItemMenu`), rows that glide to a new order, virtualized rows, loading placeholders and `hasMore` / `onLoadMore`. | [COMPONENT.md](reference/components/chat-list/COMPONENT.md) · [examples](reference/components/chat-list/examples/) |
|
|
87
88
|
|
|
88
89
|
## Status and loading (`status`)
|
|
89
90
|
|
|
@@ -17,7 +17,7 @@ is, and is shown live with its code in the playground (category «Компози
|
|
|
17
17
|
| Create in a side panel | [form-drawer.tsx](patterns/form-drawer.tsx) | Drawer form: groups, errors on submit, focus on the first invalid field, loading, toast |
|
|
18
18
|
| Dashboard | [dashboard.tsx](patterns/dashboard.tsx) | period switch, KPI row that cross-fades to the new period, a Sparkline revenue trend, invoiced vs paid in a ChartFrame, panels with progress and a split, short table + link to the list |
|
|
19
19
|
| Screen states | [screen-states.tsx](patterns/screen-states.tsx) | page error in a Banner with retry, region loading with a Skeleton, Crossfade between states, table error in place, first-run EmptyPage |
|
|
20
|
-
| Messenger | [chat.tsx](patterns/chat.tsx) | PageContent.Split of
|
|
20
|
+
| Messenger | [chat.tsx](patterns/chat.tsx) | PageContent.Split of a `ChatList` (live last message, ticks, unread counts, «печатает…») and a `Chat`: `Chat.Header` with the back button, avatar, title, the status that turns into «печатает…» (`typing`) and the search button, `Chat.Search`, `Chat.Pinned`, `Chat.Messages` from `items` + a stable `renderMessage` (dates, runs, the unread band and service events from the data, history loaded above with `hasOlder` / `onLoadOlder`), every kind of Message (photos, file, contact, voice in a VoicePlayer with a transcript, round video), reactions, reply and edit from the menu through `Composer.Context value` (`replyTo` / `editOf`), the jump to the quoted message (`Message.Reply target`), optimistic sends whose messages rise from the composer while the client's answer slides in, «печатает…» both ways (`onActivityChange` → `Chat.Root typing`), unread counts from `onNewMessage` and `onRead`, selection with `Chat.SelectionBar`, delivery ticks, the Composer in `Chat.Footer` |
|
|
21
21
|
| Sales pipeline board | [kanban-board.tsx](patterns/kanban-board.tsx) | header with one primary action, PageToolbar with SmartFilter search and a board / list SegmentedControl, Crossfade between a Kanban and a DataTable of the same deals, column totals in `renderColumnMeta`, moves saved through an `onValueChange` promise that the server can refuse, movable stages with locked first and last, the deal in a Drawer with a DescriptionList and stage actions, skeleton first load |
|
|
22
22
|
| App frame for desktop and phone (root layout) | [app-shell.tsx](patterns/app-shell.tsx) | Sidebar rail on a wide screen, floating BottomNav in `AppShell.Footer` on a phone («Ещё» opens the Sidebar panel), AppHeader with the menu button, title and search to CommandMenu, counts on items, a home page that cross-fades between sections |
|
|
23
23
|
| Media library | [media-library.tsx](patterns/media-library.tsx) | PageToolbar with sections and counts, search (`SmartFilter` with `fields={[]}`), order and «Загрузить»; uploads through `useUploadQueue` in `FileUpload` tiles (`trigger="tile"`, `layout="grid"`) with progress, a failure and retry, a done file moving into the library; a grid of `Thumbnail` / `Thumbnail.Video` tiles that open `MediaViewer` out of the tile (`getOrigin`, `thumbnails`, `renderActions`) or, in selection mode, are picked (`checked`) for bulk download and a `ConfirmDialog` delete; a Skeleton first load and a filtered `EmptyPage` in a `Crossfade` |
|
package/dist/skill/layouts.md
CHANGED
|
@@ -4,6 +4,8 @@ Every screen sits in one frame: `AppShell.Root` with Nav / `AppHeader` / Main (t
|
|
|
4
4
|
the nav rail one layer above it) → `PageContent` (header + body). `AppShell.Main` carries the page gutters
|
|
5
5
|
and `PageContent.Body` spaces its blocks 40 apart, so your code adds **no outer padding and no
|
|
6
6
|
margins**. Your CSS only lays out the inside of a block (grids, rows) with `gap` on `--prime-space-*`.
|
|
7
|
+
Main's top space under the header is part of the frame — 24, then 40 from 1024px: a page header starts
|
|
8
|
+
there; never cancel it with a negative margin or a padding override.
|
|
7
9
|
|
|
8
10
|
The page recipes are whole, working screens in [patterns/](patterns/) — start from the closest one
|
|
9
11
|
([the list](composition.md#patterns)) and keep its skeleton. The rules behind them are in [composition.md](composition.md).
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Button,
|
|
5
5
|
Card,
|
|
6
6
|
Chat,
|
|
7
|
+
ChatList,
|
|
7
8
|
Composer,
|
|
8
9
|
type ComposerActivity,
|
|
9
10
|
type ComposerMediaItem,
|
|
@@ -11,14 +12,13 @@ import {
|
|
|
11
12
|
type ComposerRecording,
|
|
12
13
|
Dropdown,
|
|
13
14
|
type GalleryItem,
|
|
15
|
+
hueOf,
|
|
14
16
|
Icon,
|
|
15
|
-
List,
|
|
16
17
|
Message,
|
|
17
18
|
type MessageReaction,
|
|
18
19
|
type MessageStatus,
|
|
19
20
|
type MessageUser,
|
|
20
21
|
PageContent,
|
|
21
|
-
ScrollContainer,
|
|
22
22
|
type UploadContext,
|
|
23
23
|
useUploadQueue,
|
|
24
24
|
VideoNote,
|
|
@@ -50,7 +50,14 @@ type ChatMessage = Content & {
|
|
|
50
50
|
reactions?: MessageReaction[];
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
type Dialog = {
|
|
53
|
+
type Dialog = {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
initials: string;
|
|
57
|
+
unread: number;
|
|
58
|
+
preview: string;
|
|
59
|
+
at: Date;
|
|
60
|
+
};
|
|
54
61
|
|
|
55
62
|
const NOW = new Date(2026, 9, 8, 15, 0);
|
|
56
63
|
const at = (day: number, hour: number, minute: number) => new Date(2026, 9, day, hour, minute);
|
|
@@ -86,9 +93,30 @@ const photo = (seed: string, width: number, height: number, alt: string): Galler
|
|
|
86
93
|
});
|
|
87
94
|
|
|
88
95
|
const DIALOGS: Dialog[] = [
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
{
|
|
97
|
+
id: "vector",
|
|
98
|
+
name: "ООО «Вектор»",
|
|
99
|
+
initials: "ВК",
|
|
100
|
+
unread: 2,
|
|
101
|
+
preview: "Ок, жду акт",
|
|
102
|
+
at: at(8, 14, 31),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "granit",
|
|
106
|
+
name: "ООО «Гранит»",
|
|
107
|
+
initials: "ГР",
|
|
108
|
+
unread: 0,
|
|
109
|
+
preview: "Акт сверки подписан",
|
|
110
|
+
at: at(7, 16, 5),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "kozlov",
|
|
114
|
+
name: "ИП Козлов",
|
|
115
|
+
initials: "ИК",
|
|
116
|
+
unread: 1,
|
|
117
|
+
preview: "Подключите ЭДО",
|
|
118
|
+
at: at(6, 11, 40),
|
|
119
|
+
},
|
|
92
120
|
];
|
|
93
121
|
|
|
94
122
|
/** Older history, loaded when the conversation is scrolled to its start. */
|
|
@@ -239,6 +267,7 @@ const MESSAGE_SEND_MS = 700;
|
|
|
239
267
|
const MESSAGE_READ_MS = 1500;
|
|
240
268
|
const CLIENT_TYPING_MS = 1800;
|
|
241
269
|
const HISTORY_MS = 900;
|
|
270
|
+
const DIALOG_MS = 800;
|
|
242
271
|
const TRANSCRIBE_MS = 1400;
|
|
243
272
|
|
|
244
273
|
/** A stand-in for the file server: sends an attachment in a few steps. */
|
|
@@ -521,6 +550,13 @@ export default function ChatPattern() {
|
|
|
521
550
|
};
|
|
522
551
|
}, []);
|
|
523
552
|
const dialog = DIALOGS.find((item) => item.id === dialogId) ?? DIALOGS[0];
|
|
553
|
+
/** The dialog whose history has arrived: opening another one loads it first. */
|
|
554
|
+
const [loadedId, setLoadedId] = React.useState<string | null>(null);
|
|
555
|
+
React.useEffect(() => {
|
|
556
|
+
if (!dialogId) return;
|
|
557
|
+
const timer = window.setTimeout(() => setLoadedId(dialogId), DIALOG_MS);
|
|
558
|
+
return () => window.clearTimeout(timer);
|
|
559
|
+
}, [dialogId]);
|
|
524
560
|
|
|
525
561
|
const loadOlder = async () => {
|
|
526
562
|
if (!hasOlder) return;
|
|
@@ -728,27 +764,36 @@ export default function ChatPattern() {
|
|
|
728
764
|
>
|
|
729
765
|
<PageContent.List aria-label="Диалоги">
|
|
730
766
|
<Card.Root className={styles.paneCard}>
|
|
731
|
-
<
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
767
|
+
<ChatList.Root
|
|
768
|
+
items={DIALOGS}
|
|
769
|
+
getKey={(item) => item.id}
|
|
770
|
+
value={dialogId}
|
|
771
|
+
onValueChange={setDialogId}
|
|
772
|
+
now={NOW}
|
|
773
|
+
renderItem={(item) => {
|
|
774
|
+
// The open dialog shows its live last message; the others their stored one.
|
|
775
|
+
const last = item.id === dialogId ? messages[messages.length - 1] : undefined;
|
|
776
|
+
return (
|
|
777
|
+
<ChatList.Item
|
|
778
|
+
time={last?.sentAt ?? item.at}
|
|
779
|
+
status={last?.from === "me" ? last.status : undefined}
|
|
780
|
+
unread={unread[item.id] ?? 0}
|
|
781
|
+
typing={item.id === dialogId && clientTyping}
|
|
738
782
|
>
|
|
739
|
-
<
|
|
740
|
-
<Avatar.Root color=
|
|
783
|
+
<ChatList.ItemIcon>
|
|
784
|
+
<Avatar.Root color={hueOf(item.id)}>
|
|
741
785
|
<Avatar.Fallback>{item.initials}</Avatar.Fallback>
|
|
786
|
+
{item.id === "vector" ? <Avatar.Status status="online" /> : null}
|
|
742
787
|
</Avatar.Root>
|
|
743
|
-
</
|
|
744
|
-
<
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
</
|
|
749
|
-
)
|
|
750
|
-
|
|
751
|
-
|
|
788
|
+
</ChatList.ItemIcon>
|
|
789
|
+
<ChatList.ItemText>{item.name}</ChatList.ItemText>
|
|
790
|
+
<ChatList.ItemDescription author={last?.from === "me" ? "Вы" : undefined}>
|
|
791
|
+
{last ? excerpt(last) : item.preview}
|
|
792
|
+
</ChatList.ItemDescription>
|
|
793
|
+
</ChatList.Item>
|
|
794
|
+
);
|
|
795
|
+
}}
|
|
796
|
+
/>
|
|
752
797
|
</Card.Root>
|
|
753
798
|
</PageContent.List>
|
|
754
799
|
|
|
@@ -757,7 +802,7 @@ export default function ChatPattern() {
|
|
|
757
802
|
<Chat.Root typing={clientTyping} selected={selected} onSelectedChange={setSelected}>
|
|
758
803
|
<Chat.Header>
|
|
759
804
|
<PageContent.BackButton />
|
|
760
|
-
<Avatar.Root color=
|
|
805
|
+
<Avatar.Root color={hueOf(dialog.id)}>
|
|
761
806
|
<Avatar.Fallback>{dialog.initials}</Avatar.Fallback>
|
|
762
807
|
<Avatar.Status status="online" />
|
|
763
808
|
</Avatar.Root>
|
|
@@ -770,6 +815,7 @@ export default function ChatPattern() {
|
|
|
770
815
|
<Chat.Search />
|
|
771
816
|
<Chat.Pinned items={pinnedItems} onRemove={togglePin} />
|
|
772
817
|
<Chat.Messages
|
|
818
|
+
loading={loadedId !== dialogId}
|
|
773
819
|
reader={ME}
|
|
774
820
|
items={messages}
|
|
775
821
|
getKey={(message) => message.id}
|
|
@@ -471,15 +471,9 @@ export default function KanbanBoardPattern() {
|
|
|
471
471
|
}}
|
|
472
472
|
renderColumnMeta={figures}
|
|
473
473
|
loading={loading}
|
|
474
|
+
onItemClick={(item) => openDeal(item.id)}
|
|
474
475
|
renderItem={(item) => (
|
|
475
|
-
<Kanban.Item
|
|
476
|
-
onClick={() => openDeal(item.id)}
|
|
477
|
-
onKeyDown={(event) => {
|
|
478
|
-
if (event.key === "Enter" && event.target === event.currentTarget) {
|
|
479
|
-
openDeal(item.id);
|
|
480
|
-
}
|
|
481
|
-
}}
|
|
482
|
-
>
|
|
476
|
+
<Kanban.Item>
|
|
483
477
|
<Kanban.ItemTitle>{item.company}</Kanban.ItemTitle>
|
|
484
478
|
<Kanban.ItemDescription>
|
|
485
479
|
{RUBLES.format(item.amount)} · до {DAY.format(new Date(item.due))}
|
|
@@ -530,10 +530,22 @@ export default function SchedulePattern() {
|
|
|
530
530
|
setSelected({ bar, person: findPerson(row.id) });
|
|
531
531
|
setDetailOpen(true);
|
|
532
532
|
}}
|
|
533
|
+
renderBarTooltip={(bar) => (bar.client ? CLIENTS[bar.client].name : KIND[bar.kind])}
|
|
533
534
|
toolbar={
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
535
|
+
<>
|
|
536
|
+
<Typography as="span" variant="body-s" tone="muted">
|
|
537
|
+
{loading
|
|
538
|
+
? "Загружаем график…"
|
|
539
|
+
: `Сотрудников: ${rows.length} из ${people.length}`}
|
|
540
|
+
</Typography>
|
|
541
|
+
{/* The legend of client hues: the bar's hue repeats its client, the tooltip names it. */}
|
|
542
|
+
{(Object.keys(CLIENTS) as ClientId[]).map((client) => (
|
|
543
|
+
<Badge.Root key={client} color={CLIENTS[client].color}>
|
|
544
|
+
<Badge.Dot />
|
|
545
|
+
{CLIENTS[client].name}
|
|
546
|
+
</Badge.Root>
|
|
547
|
+
))}
|
|
548
|
+
</>
|
|
537
549
|
}
|
|
538
550
|
empty={
|
|
539
551
|
<EmptyPage.Root size="s" role="status" aria-labelledby="schedule-empty">
|