@konce-pt/react 0.8.3 → 0.9.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 +4 -4
- package/dist/backdrop/backdrop.d.ts +79 -0
- package/dist/backdrop/backdrop.js +195 -0
- package/dist/backdrop/llms.txt +132 -0
- package/dist/button/button.d.ts +23 -7
- package/dist/button/button.js +33 -9
- package/dist/button/llms.txt +48 -12
- package/dist/button-group/button-group.d.ts +7 -1
- package/dist/button-group/button-group.js +9 -4
- package/dist/button-group/llms.txt +30 -5
- package/dist/chat/chat.d.ts +53 -0
- package/dist/chat/chat.js +160 -0
- package/dist/chat/llms.txt +78 -0
- package/dist/chat-composer/chat-composer.d.ts +41 -0
- package/dist/chat-composer/chat-composer.js +81 -0
- package/dist/chat-composer/llms.txt +56 -0
- package/dist/chat-dock/chat-dock.d.ts +40 -0
- package/dist/chat-dock/chat-dock.js +67 -0
- package/dist/chat-dock/llms.txt +71 -0
- package/dist/chat-entry/public-api.d.ts +12 -0
- package/dist/chat-entry/public-api.js +10 -0
- package/dist/chat-message/chat-message.d.ts +53 -0
- package/dist/chat-message/chat-message.js +112 -0
- package/dist/chat-message/llms.txt +73 -0
- package/dist/chat-typing/chat-typing.d.ts +23 -0
- package/dist/chat-typing/chat-typing.js +30 -0
- package/dist/chat-typing/llms.txt +41 -0
- package/dist/chip/chip.d.ts +10 -1
- package/dist/chip/chip.js +3 -2
- package/dist/chip/llms.txt +9 -0
- package/dist/data-view/data-view.d.ts +15 -3
- package/dist/data-view/data-view.js +22 -5
- package/dist/data-view/llms.txt +20 -7
- package/dist/dialog/dialog.d.ts +9 -1
- package/dist/dialog/dialog.js +2 -2
- package/dist/internal/dev.d.ts +1 -0
- package/dist/internal/dev.js +3 -0
- package/dist/internal/overlay.d.ts +6 -1
- package/dist/internal/overlay.js +5 -3
- package/dist/mention/llms.txt +60 -0
- package/dist/mention/mention.d.ts +38 -0
- package/dist/mention/mention.js +29 -0
- package/dist/mention/mentions.d.ts +31 -0
- package/dist/mention/mentions.js +24 -0
- package/dist/order-list/llms.txt +16 -5
- package/dist/order-list/order-list.d.ts +5 -3
- package/dist/order-list/order-list.js +69 -4
- package/dist/peek/llms.txt +42 -0
- package/dist/peek/peek.d.ts +35 -0
- package/dist/peek/peek.js +127 -0
- package/dist/pick-list/llms.txt +19 -5
- package/dist/pick-list/pick-list.d.ts +7 -2
- package/dist/pick-list/pick-list.js +123 -17
- package/dist/progress/llms.txt +70 -22
- package/dist/progress/progress.d.ts +34 -7
- package/dist/progress/progress.js +46 -9
- package/dist/public-api.d.ts +15 -3
- package/dist/public-api.js +6 -0
- package/dist/rich-text/llms.txt +10 -0
- package/dist/rich-text/rich-text.d.ts +11 -1
- package/dist/rich-text/rich-text.js +157 -5
- package/dist/rich-text/sanitize.js +10 -1
- package/dist/roadmap/roadmap.js +1 -3
- package/dist/shared/types.d.ts +2 -0
- package/package.json +16 -9
package/dist/button/llms.txt
CHANGED
|
@@ -1,35 +1,68 @@
|
|
|
1
1
|
# KptButton (@konce-pt/react)
|
|
2
2
|
|
|
3
|
-
The Koncept UI button. It renders a native `<button>` (semantics + form submit)
|
|
4
|
-
`.kpt-button-host` wrapper
|
|
5
|
-
carries its own styles. Import: `import { KptButton } from '@konce-pt/react';`
|
|
3
|
+
The Koncept UI button. It renders a native `<button>` (semantics + form submit) — or an `<a>` when
|
|
4
|
+
you pass `href` — inside a `.kpt-button-host` wrapper: two DOM levels, exactly like the Angular port,
|
|
5
|
+
because the host carries its own styles. Import: `import { KptButton } from '@konce-pt/react';`
|
|
6
6
|
|
|
7
7
|
## DOM
|
|
8
8
|
<span class="kpt-button-host">
|
|
9
9
|
<button class="kpt-button" type="button" data-variant="filled" data-size="md">
|
|
10
|
+
<span class="kpt-button__icon kpt-icon-host">…</span>
|
|
10
11
|
<span class="kpt-button__content">…</span>
|
|
12
|
+
<span class="kpt-button__icon kpt-button__icon--end kpt-icon-host">…</span>
|
|
11
13
|
</button>
|
|
12
14
|
</span>
|
|
13
15
|
|
|
14
16
|
## Props
|
|
15
|
-
- `variant`: 'filled' | 'tonal' | 'outline' | 'text' | 'danger' (default 'filled')
|
|
16
|
-
- `size`: 'sm' | 'md' | 'lg' (default 'md')
|
|
17
|
+
- `variant`: 'filled' | 'elevated' | 'tonal' | 'outline' | 'text' | 'link' | 'danger' | 'success' | 'warning' | 'info' (default 'filled')
|
|
18
|
+
- `size`: 'xs' | 'sm' | 'md' | 'lg' | 'xl' (default 'md')
|
|
17
19
|
- `type`: 'button' | 'submit' | 'reset' (default 'button')
|
|
18
20
|
- `disabled`: boolean
|
|
19
21
|
- `loading`: boolean — a spinner + blocked interaction, sets `aria-busy`
|
|
20
22
|
- `fullWidth`: boolean — the full width of the container
|
|
23
|
+
- `icon` / `iconEnd`: string — a name from the icon registry
|
|
24
|
+
- `iconClassName` / `iconEndClassName`: string — an extra class on that icon
|
|
25
|
+
- `href`, `target`, `rel`: string — render an `<a>` instead of a `<button>`
|
|
21
26
|
- `asChild`: boolean — render the single child instead of `<button>`, keeping every class
|
|
22
27
|
- `hostProps`: props for the `.kpt-button-host` wrapper
|
|
23
|
-
- `className`, `ref` and every other `<button>` prop go to the INNER
|
|
28
|
+
- `className`, `ref` and every other `<button>` prop go to the INNER element
|
|
24
29
|
|
|
25
30
|
## Events
|
|
26
31
|
No events of its own — bind `onClick` and it lands on the native `<button>`.
|
|
27
32
|
|
|
33
|
+
## Icons
|
|
34
|
+
Two ways in, one result. `icon` and `iconEnd` take a name from the icon registry and the component
|
|
35
|
+
renders it; anything put in `children` is laid out the same way. Both get the gap from
|
|
36
|
+
`--kpt-button-gap` and a size that follows `size` (0.875rem at `xs` up to 1.5rem at `xl`) — the
|
|
37
|
+
content box is a flex container, so an icon sits on the centre line rather than on the text baseline.
|
|
38
|
+
|
|
39
|
+
`loading` replaces the leading icon with the spinner instead of standing next to it, and the
|
|
40
|
+
spinner takes that icon's size, so the label does not jump when loading starts. The trailing icon
|
|
41
|
+
stays put — it usually marks direction rather than the action.
|
|
42
|
+
|
|
43
|
+
`iconClassName` / `iconEndClassName` add a class next to the permanent `.kpt-button__icon` hook, for
|
|
44
|
+
the cases where one instance needs a different colour or a rotation.
|
|
45
|
+
|
|
46
|
+
## Link and the `link` variant
|
|
47
|
+
Two different things. `variant="link"` only changes the looks — underline, transparent background,
|
|
48
|
+
no side padding, so it sits inside a sentence. `href` changes the element: the component renders
|
|
49
|
+
`<a class="kpt-button">` instead of `<button>`, and the two combine freely (a filled button can be
|
|
50
|
+
a link, a `link` variant can be a plain button). `asChild` wins over `href`: whoever substitutes
|
|
51
|
+
their own element decides the tag.
|
|
52
|
+
|
|
53
|
+
A disabled link loses its `href` rather than just gaining an attribute — otherwise it would stay
|
|
54
|
+
clickable and reachable with the keyboard; it also gets `aria-disabled="true"` and `tabindex="-1"`.
|
|
55
|
+
`target="_blank"` without an explicit `rel` gets `rel="noopener noreferrer"`.
|
|
56
|
+
|
|
28
57
|
## Examples
|
|
29
58
|
<KptButton onClick={save}>Save</KptButton>
|
|
30
|
-
<KptButton
|
|
59
|
+
<KptButton icon="device-floppy" onClick={save}>Save</KptButton>
|
|
60
|
+
<KptButton iconEnd="chevron-right" variant="outline">Next</KptButton>
|
|
61
|
+
<KptButton icon="printer" variant="text" size="sm">Print</KptButton>
|
|
62
|
+
<KptButton variant="success">Approve</KptButton>
|
|
63
|
+
<KptButton variant="link" href="/docs">Documentation</KptButton>
|
|
31
64
|
<KptButton variant="danger" loading={deleting}>Delete</KptButton>
|
|
32
|
-
<
|
|
65
|
+
<KptButton variant="outline"><KptIcon name="printer" />Print</KptButton>
|
|
33
66
|
<KptButton asChild><a href="/docs">Documentation</a></KptButton>
|
|
34
67
|
|
|
35
68
|
## asChild
|
|
@@ -39,10 +72,12 @@ element gets `disabled` when it is a native `<button>`, and `aria-disabled` othe
|
|
|
39
72
|
element with no `:disabled` state therefore does not get the dimmed look.
|
|
40
73
|
|
|
41
74
|
## Tokens (styling)
|
|
42
|
-
`--kpt-button-radius`, `--kpt-button-font-weight`, `--kpt-button-gap`,
|
|
43
|
-
`--kpt-button-filled-{bg,fg,bg-hover,bg-active}`, `--kpt-button-
|
|
44
|
-
`--kpt-button-
|
|
45
|
-
|
|
75
|
+
`--kpt-button-radius`, `--kpt-button-font-weight`, `--kpt-button-gap`, and one group per variant:
|
|
76
|
+
`--kpt-button-filled-{bg,fg,bg-hover,bg-active}`, `--kpt-button-elevated-{bg,fg,bg-hover}`,
|
|
77
|
+
`--kpt-button-tonal-{bg,fg,bg-hover}`, `--kpt-button-outline-{border,fg,bg-hover}`,
|
|
78
|
+
`--kpt-button-text-{fg,bg-hover}`, `--kpt-button-link-{fg,fg-hover}`,
|
|
79
|
+
`--kpt-button-danger-{bg,fg,bg-hover}` and the same triplet for success, warning and info.
|
|
80
|
+
The elevated variant also reaches for `--kpt-shadow-sm` and `--kpt-shadow-md`.
|
|
46
81
|
|
|
47
82
|
## The state layer (hover / active)
|
|
48
83
|
Hover and active feedback is carried by an `::after` pseudo-element (`opacity` 0 → 1), not by
|
|
@@ -53,4 +88,5 @@ along with the stylesheet.
|
|
|
53
88
|
## Accessibility
|
|
54
89
|
- A native `<button>` — full keyboard and screen reader support.
|
|
55
90
|
- `loading` sets `aria-busy="true"` and disables the button.
|
|
91
|
+
- An `<a>` matches no `:disabled`, so the disabled look keys off `[aria-disabled='true']` as well.
|
|
56
92
|
- The focus ring comes from `@konce-pt/styles` (`--kpt-color-focus-ring`).
|
|
@@ -10,6 +10,8 @@ export interface KptButtonGroupProps<T> extends Omit<ComponentPropsWithRef<'div'
|
|
|
10
10
|
disabled?: boolean;
|
|
11
11
|
/** Wywoływane po wyborze — moment, w którym kontrolka staje się dotknięta. */
|
|
12
12
|
onTouch?: () => void;
|
|
13
|
+
/** Chowa etykiety — zostaje sama ikona, a `label` zasila `aria-label` i `title`. */
|
|
14
|
+
iconOnly?: boolean;
|
|
13
15
|
/** Propsy wrappera hosta (`.kpt-button-group-host`). */
|
|
14
16
|
hostProps?: KptHostProps<'span'>;
|
|
15
17
|
}
|
|
@@ -17,9 +19,13 @@ export interface KptButtonGroupProps<T> extends Omit<ComponentPropsWithRef<'div'
|
|
|
17
19
|
* Segmentowana grupa przycisków (toggle) — pojedynczy wybór z opcji.
|
|
18
20
|
*
|
|
19
21
|
* Działa kontrolowana (`value` + `onValueChange`) i niekontrolowana (`defaultValue`).
|
|
22
|
+
* Pozycja może mieć ikonę (`KptOption.icon`); `iconOnly` chowa etykietę, ale zostawia ją
|
|
23
|
+
* w nazwie dostępnej, więc czytnik ekranu dalej wie, co wybiera.
|
|
20
24
|
*
|
|
21
25
|
* @example
|
|
22
26
|
* <KptButtonGroup value={view} onValueChange={setView}
|
|
23
27
|
* options={[{ label: 'Lista', value: 'list' }, { label: 'Siatka', value: 'grid' }]} />
|
|
28
|
+
* <KptButtonGroup value={view} onValueChange={setView} iconOnly
|
|
29
|
+
* options={[{ icon: 'list', label: 'Lista', value: 'list' }]} />
|
|
24
30
|
*/
|
|
25
|
-
export declare function KptButtonGroup<T>({ options, value, defaultValue, onValueChange, size, disabled, onTouch, hostProps, className, ...rest }: Readonly<KptButtonGroupProps<T>>): import("react").JSX.Element;
|
|
31
|
+
export declare function KptButtonGroup<T>({ options, value, defaultValue, onValueChange, size, disabled, onTouch, iconOnly, hostProps, className, ...rest }: Readonly<KptButtonGroupProps<T>>): import("react").JSX.Element;
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../internal/cn.js";
|
|
3
3
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
4
|
+
import { KptIcon } from "../icon/icon.js";
|
|
4
5
|
/**
|
|
5
6
|
* Segmentowana grupa przycisków (toggle) — pojedynczy wybór z opcji.
|
|
6
7
|
*
|
|
7
8
|
* Działa kontrolowana (`value` + `onValueChange`) i niekontrolowana (`defaultValue`).
|
|
9
|
+
* Pozycja może mieć ikonę (`KptOption.icon`); `iconOnly` chowa etykietę, ale zostawia ją
|
|
10
|
+
* w nazwie dostępnej, więc czytnik ekranu dalej wie, co wybiera.
|
|
8
11
|
*
|
|
9
12
|
* @example
|
|
10
13
|
* <KptButtonGroup value={view} onValueChange={setView}
|
|
11
14
|
* options={[{ label: 'Lista', value: 'list' }, { label: 'Siatka', value: 'grid' }]} />
|
|
15
|
+
* <KptButtonGroup value={view} onValueChange={setView} iconOnly
|
|
16
|
+
* options={[{ icon: 'list', label: 'Lista', value: 'list' }]} />
|
|
12
17
|
*/
|
|
13
|
-
export function KptButtonGroup({ options = [], value, defaultValue, onValueChange, size = 'md', disabled = false, onTouch, hostProps, className, ...rest }) {
|
|
18
|
+
export function KptButtonGroup({ options = [], value, defaultValue, onValueChange, size = 'md', disabled = false, onTouch, iconOnly = false, hostProps, className, ...rest }) {
|
|
14
19
|
const { className: hostClassName, ...hostRest } = hostProps ?? {};
|
|
15
20
|
const [current, setCurrent] = useControllableState({
|
|
16
21
|
value,
|
|
@@ -23,8 +28,8 @@ export function KptButtonGroup({ options = [], value, defaultValue, onValueChang
|
|
|
23
28
|
setCurrent(option.value);
|
|
24
29
|
onTouch?.();
|
|
25
30
|
};
|
|
26
|
-
return (_jsx("span", { className: cn('kpt-button-group-host', hostClassName), ...hostRest, children: _jsx("div", { className: cn('kpt-button-group', className), role: "group", "data-size": size, ...rest, children: options.map((option) => {
|
|
31
|
+
return (_jsx("span", { className: cn('kpt-button-group-host', hostClassName), ...hostRest, children: _jsx("div", { className: cn('kpt-button-group', className), role: "group", "data-size": size, "data-icon-only": iconOnly ? '' : undefined, ...rest, children: options.map((option) => {
|
|
27
32
|
const active = option.value === current;
|
|
28
|
-
return (
|
|
33
|
+
return (_jsxs("button", { type: "button", className: cn('kpt-button-group__item', active && 'is-active'), disabled: disabled || option.disabled, "aria-pressed": active, "aria-label": iconOnly ? option.label : undefined, title: iconOnly ? option.label : undefined, onClick: () => select(option), children: [option.icon ? _jsx(KptIcon, { className: "kpt-button-group__icon", name: option.icon }) : null, iconOnly ? null : _jsx("span", { className: "kpt-button-group__label", children: option.label })] }, String(option.value)));
|
|
29
34
|
}) }) }));
|
|
30
35
|
}
|
|
@@ -6,20 +6,39 @@ Import: `import { KptButtonGroup } from '@konce-pt/react';`
|
|
|
6
6
|
## DOM
|
|
7
7
|
<span class="kpt-button-group-host">
|
|
8
8
|
<div class="kpt-button-group" role="group" data-size="md">
|
|
9
|
-
<button type="button" class="kpt-button-group__item is-active" aria-pressed="true">
|
|
10
|
-
|
|
9
|
+
<button type="button" class="kpt-button-group__item is-active" aria-pressed="true">
|
|
10
|
+
<span class="kpt-button-group__icon kpt-icon-host">…</span>
|
|
11
|
+
<span class="kpt-button-group__label">List</span>
|
|
12
|
+
</button>
|
|
13
|
+
<button type="button" class="kpt-button-group__item" aria-pressed="false">
|
|
14
|
+
<span class="kpt-button-group__label">Grid</span>
|
|
15
|
+
</button>
|
|
11
16
|
</div>
|
|
12
17
|
</span>
|
|
13
18
|
|
|
14
19
|
## Props
|
|
15
|
-
- `options`: `KptOption<T>[]` — `{ label, value, disabled? }`
|
|
20
|
+
- `options`: `KptOption<T>[]` — `{ label, value, disabled?, icon? }`
|
|
16
21
|
- `value` / `defaultValue` / `onValueChange` — controlled or uncontrolled
|
|
17
22
|
- `size`: 'sm' | 'md' | 'lg' (default 'md')
|
|
18
23
|
- `disabled`: boolean — disables the whole group; a single option disables itself
|
|
24
|
+
- `iconOnly`: boolean — hides the labels and leaves the icons alone
|
|
19
25
|
- `onTouch()` — fires on a pick, the moment the control becomes touched
|
|
20
26
|
- `hostProps` — props for the `.kpt-button-group-host` wrapper
|
|
21
27
|
- `className`, `ref` and every other `<div>` prop go to `.kpt-button-group`
|
|
22
28
|
|
|
29
|
+
## Icons
|
|
30
|
+
An option carries its icon by name: `KptOption.icon` is a key from the icon registry, and the group
|
|
31
|
+
renders it before the label. The item is a flex box, so the icon gets the `--kpt-button-gap` spacing
|
|
32
|
+
and a size that follows `size` (1rem at `sm` up to 1.25rem at `lg`) — the same scale as `KptButton`,
|
|
33
|
+
so a group standing next to a button matches it.
|
|
34
|
+
|
|
35
|
+
`iconOnly` drops the label from the page but keeps it as the accessible name: the item gets both
|
|
36
|
+
`aria-label` and `title`, so a screen reader still says "Grid" and a mouse user gets a tooltip.
|
|
37
|
+
Each item also becomes a square, because a lone icon inside a 1rem side padding looks adrift.
|
|
38
|
+
|
|
39
|
+
The icon takes its colour from `currentColor`, so the active item — filled with
|
|
40
|
+
`--kpt-color-primary` — flips the icon to `--kpt-color-on-primary` with no extra rule.
|
|
41
|
+
|
|
23
42
|
## Values of any type
|
|
24
43
|
The value is generic, so options can carry objects, not just strings. Keys come from
|
|
25
44
|
`String(option.value)`, so distinct options need distinct string forms — for object values give
|
|
@@ -29,12 +48,18 @@ each one a stable id and map back to the object yourself.
|
|
|
29
48
|
<KptButtonGroup value={view} onValueChange={setView}
|
|
30
49
|
options={[{ label: 'List', value: 'list' }, { label: 'Grid', value: 'grid' }]} />
|
|
31
50
|
|
|
51
|
+
<KptButtonGroup value={view} onValueChange={setView}
|
|
52
|
+
options={[{ icon: 'list', label: 'List', value: 'list' }]} />
|
|
53
|
+
|
|
54
|
+
<KptButtonGroup value={align} onValueChange={setAlign} iconOnly options={alignOptions} />
|
|
55
|
+
|
|
32
56
|
<KptButtonGroup size="sm" defaultValue="M" options={sizeOptions} />
|
|
33
57
|
|
|
34
58
|
## Tokens
|
|
35
59
|
`--kpt-color-surface`, `--kpt-color-muted`, `--kpt-color-primary` (the active segment),
|
|
36
|
-
`--kpt-color-border-strong`, `--kpt-radius-md`.
|
|
60
|
+
`--kpt-color-border-strong`, `--kpt-radius-md`, `--kpt-button-gap`.
|
|
37
61
|
|
|
38
62
|
## Accessibility
|
|
39
63
|
A `role="group"` of native buttons, each with `aria-pressed` — a screen reader announces which
|
|
40
|
-
segment is on, and the keyboard walks them like any other buttons.
|
|
64
|
+
segment is on, and the keyboard walks them like any other buttons. With `iconOnly` the name comes
|
|
65
|
+
from `aria-label`, so the announcement does not degrade to a bare "button".
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
|
+
import type { KptChatAuthor, KptChatMessage as KptChatMessageModel, KptChatMessageRow, KptChatReactionOption, KptChatReplyRef } from '@konce-pt/chat';
|
|
3
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
4
|
+
/** Wybór reakcji na wiadomości — komponent mówi, w co kliknięto, dopisanie należy do aplikacji. */
|
|
5
|
+
export interface KptChatReactEvent<T = unknown> {
|
|
6
|
+
message: KptChatMessageModel<T>;
|
|
7
|
+
emoji: string;
|
|
8
|
+
}
|
|
9
|
+
export interface KptChatProps<T = unknown> extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
|
|
10
|
+
/** Wiadomości w kolejności chronologicznej — komponent ich nie sortuje. */
|
|
11
|
+
messages?: readonly KptChatMessageModel<T>[];
|
|
12
|
+
/** `id` nadawcy uznawanego za „mnie": jego wiadomości idą na prawo i mają status. */
|
|
13
|
+
currentUser?: string;
|
|
14
|
+
/** Przerwa łamiąca grupę wiadomości jednego nadawcy. */
|
|
15
|
+
groupGapMs?: number;
|
|
16
|
+
/** `id` pierwszej nieprzeczytanej wiadomości — nad nią stanie dzielnik. */
|
|
17
|
+
unreadFrom?: string | null;
|
|
18
|
+
/** Brak połączenia: pasek u góry listy z liczbą wiadomości czekających na wysłanie. */
|
|
19
|
+
offline?: boolean;
|
|
20
|
+
/** Osoby piszące w tej chwili. */
|
|
21
|
+
typing?: readonly KptChatAuthor[];
|
|
22
|
+
/** Dosuwanie widoku do najnowszej wiadomości, dopóki użytkownik nie odjedzie w górę. */
|
|
23
|
+
autoScroll?: boolean;
|
|
24
|
+
/** Akcje bąbla pod kursorem: odpowiedz i zareaguj. */
|
|
25
|
+
actions?: boolean;
|
|
26
|
+
/** Zestaw wybieraka reakcji, przekazywany bąblom. Domyślnie pięć wbudowanych. */
|
|
27
|
+
reactionOptions?: readonly KptChatReactionOption[];
|
|
28
|
+
/** Własny komunikat pustej rozmowy. */
|
|
29
|
+
emptyLabel?: string;
|
|
30
|
+
onRetry?: (message: KptChatMessageModel<T>) => void;
|
|
31
|
+
onReply?: (message: KptChatMessageModel<T>) => void;
|
|
32
|
+
onReact?: (event: KptChatReactEvent<T>) => void;
|
|
33
|
+
onQuoted?: (quote: KptChatReplyRef) => void;
|
|
34
|
+
/** Przewinięcie na sam szczyt — moment na dociągnięcie starszych wiadomości. */
|
|
35
|
+
onLoadMore?: () => void;
|
|
36
|
+
/** Szablon treści bąbla — odpowiednik `<ng-template let-message>` z Angulara. */
|
|
37
|
+
renderMessage?: (message: KptChatMessageModel<T>, row: KptChatMessageRow<T>) => ReactNode;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Lista rozmowy — grupuje wiadomości, wstawia separatory dni i dzielnik nieprzeczytanych,
|
|
41
|
+
* trzyma widok przy dole i pokazuje pasek offline z liczbą wiadomości czekających na wysłanie.
|
|
42
|
+
*
|
|
43
|
+
* Sama logika wierszy mieszka w `@konce-pt/chat` (`buildChatRows`), więc port Angulara liczy ją
|
|
44
|
+
* tak samo. Komponent nie ma nic wspólnego z transportem: wiadomości dostaje, a `onRetry`
|
|
45
|
+
* i `onLoadMore` tylko zgłasza — ponowieniem wysyłki i dociąganiem historii zajmuje się aplikacja.
|
|
46
|
+
*
|
|
47
|
+
* Kompozytor (`KptChatComposer`) jest osobnym komponentem i stoi obok listy — dzięki temu
|
|
48
|
+
* czat bez pola wpisywania (podgląd, archiwum) nie wozi go ze sobą.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* <KptChat messages={messages} currentUser="me" typing={typing} onRetry={resend} />
|
|
52
|
+
*/
|
|
53
|
+
export declare function KptChat<T = unknown>({ messages, currentUser, groupGapMs, unreadFrom, offline, typing, autoScroll, actions, reactionOptions, emptyLabel, onRetry, onReply, onReact, onQuoted, onLoadMore, renderMessage, locale, dictionary, className, ...rest }: Readonly<KptChatProps<T>>): import("react").JSX.Element;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { KPT_CHAT_GROUP_GAP_MS, KPT_CHAT_REACTIONS, buildChatRows, chatDayIndicator, pendingCount } from '@konce-pt/chat';
|
|
4
|
+
import { cn } from "../internal/cn.js";
|
|
5
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
6
|
+
import { KptIcon } from "../icon/icon.js";
|
|
7
|
+
import { KptChatMessage } from "../chat-message/chat-message.js";
|
|
8
|
+
import { KptChatTyping } from "../chat-typing/chat-typing.js";
|
|
9
|
+
/** Odległość od dołu (px), przy której listę uznajemy jeszcze za przypiętą. */
|
|
10
|
+
const PIN_THRESHOLD = 48;
|
|
11
|
+
/**
|
|
12
|
+
* Wiersze, które nie mogą znaleźć się pod pływającą pigułką dnia: separatory i komunikaty
|
|
13
|
+
* systemowe, czyli wszystko wyśrodkowane, co czyta się jak jej bliźniak. Bąble są poza listą —
|
|
14
|
+
* pigułka nad wypowiedzią to normalny obrazek czatu.
|
|
15
|
+
*/
|
|
16
|
+
const BLOCKING_ROWS = '.kpt-chat__date, .kpt-chat__unread, .kpt-chat-message__system';
|
|
17
|
+
/**
|
|
18
|
+
* Lista rozmowy — grupuje wiadomości, wstawia separatory dni i dzielnik nieprzeczytanych,
|
|
19
|
+
* trzyma widok przy dole i pokazuje pasek offline z liczbą wiadomości czekających na wysłanie.
|
|
20
|
+
*
|
|
21
|
+
* Sama logika wierszy mieszka w `@konce-pt/chat` (`buildChatRows`), więc port Angulara liczy ją
|
|
22
|
+
* tak samo. Komponent nie ma nic wspólnego z transportem: wiadomości dostaje, a `onRetry`
|
|
23
|
+
* i `onLoadMore` tylko zgłasza — ponowieniem wysyłki i dociąganiem historii zajmuje się aplikacja.
|
|
24
|
+
*
|
|
25
|
+
* Kompozytor (`KptChatComposer`) jest osobnym komponentem i stoi obok listy — dzięki temu
|
|
26
|
+
* czat bez pola wpisywania (podgląd, archiwum) nie wozi go ze sobą.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <KptChat messages={messages} currentUser="me" typing={typing} onRetry={resend} />
|
|
30
|
+
*/
|
|
31
|
+
export function KptChat({ messages = [], currentUser = '', groupGapMs = KPT_CHAT_GROUP_GAP_MS, unreadFrom = null, offline = false, typing = [], autoScroll = true, actions = false, reactionOptions = KPT_CHAT_REACTIONS, emptyLabel = '', onRetry, onReply, onReact, onQuoted, onLoadMore, renderMessage, locale, dictionary, className, ...rest }) {
|
|
32
|
+
const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
|
|
33
|
+
const viewportRef = useRef(null);
|
|
34
|
+
const dayRef = useRef(null);
|
|
35
|
+
/** Czy widok stoi przy dole listy. Odjazd w górę wyłącza dosuwanie. */
|
|
36
|
+
const [pinned, setPinned] = useState(true);
|
|
37
|
+
/** Ile wiadomości przyszło, odkąd użytkownik odjechał w górę. */
|
|
38
|
+
const [newCount, setNewCount] = useState(0);
|
|
39
|
+
/** Tekst pływającej pigułki dnia — zostaje po zgaszeniu, żeby jej pas nie zmieniał wysokości. */
|
|
40
|
+
const [dayLabel, setDayLabel] = useState('');
|
|
41
|
+
/** Czy pigułka dnia jest widoczna. */
|
|
42
|
+
const [dayVisible, setDayVisible] = useState(false);
|
|
43
|
+
const lastCount = useRef(0);
|
|
44
|
+
const wasAtTop = useRef(false);
|
|
45
|
+
const rows = buildChatRows(messages, { currentUserId: currentUser, gapMs: groupGapMs, unreadFrom });
|
|
46
|
+
const pending = pendingCount(messages);
|
|
47
|
+
/**
|
|
48
|
+
* Ustawia tekst pływającej pigułki dnia — co ma na niej być, liczy `chatDayIndicator`
|
|
49
|
+
* z `@konce-pt/chat`, tutaj zostaje czytanie prostokątów i przepisanie etykiety.
|
|
50
|
+
*
|
|
51
|
+
* Wszystkie pomiary idą przed zapisem stanu, więc przeglądarka przelicza układ raz
|
|
52
|
+
* na klatkę zamiast przy każdym wierszu.
|
|
53
|
+
*/
|
|
54
|
+
const syncDay = useCallback(() => {
|
|
55
|
+
const element = viewportRef.current;
|
|
56
|
+
const pill = dayRef.current;
|
|
57
|
+
if (!element || !pill)
|
|
58
|
+
return;
|
|
59
|
+
const separators = Array.from(element.querySelectorAll('.kpt-chat__date'));
|
|
60
|
+
const blockers = Array.from(element.querySelectorAll(BLOCKING_ROWS));
|
|
61
|
+
const box = (node) => {
|
|
62
|
+
const rect = node.getBoundingClientRect();
|
|
63
|
+
return { top: rect.top, bottom: rect.bottom };
|
|
64
|
+
};
|
|
65
|
+
const active = chatDayIndicator(separators.map(box), blockers.map(box), box(pill));
|
|
66
|
+
setDayVisible(active >= 0);
|
|
67
|
+
if (active < 0)
|
|
68
|
+
return;
|
|
69
|
+
// Etykieta zostaje po zgaszeniu pigułki: pusta zmieniałaby jej wysokość, a skrypt mierzy
|
|
70
|
+
// ten pas także wtedy, gdy pigułki nie widać.
|
|
71
|
+
setDayLabel(separators[active]?.textContent?.trim() ?? '');
|
|
72
|
+
}, []);
|
|
73
|
+
const scrollToBottom = (behavior) => {
|
|
74
|
+
const element = viewportRef.current;
|
|
75
|
+
if (!element)
|
|
76
|
+
return;
|
|
77
|
+
const reduced = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
78
|
+
element.scrollTo({ top: element.scrollHeight, behavior: behavior ?? (reduced ? 'auto' : 'smooth') });
|
|
79
|
+
};
|
|
80
|
+
// Dosunięcie liczone po ułożeniu, nie po malowaniu — inaczej widać skok listy.
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
if (!viewportRef.current)
|
|
83
|
+
return;
|
|
84
|
+
const count = messages.length;
|
|
85
|
+
const grew = count > lastCount.current;
|
|
86
|
+
const first = lastCount.current === 0;
|
|
87
|
+
lastCount.current = count;
|
|
88
|
+
if (!grew)
|
|
89
|
+
return;
|
|
90
|
+
// Pierwsze wejście zawsze ląduje na dole — rozmowę czyta się od końca.
|
|
91
|
+
if (first || (autoScroll && pinned))
|
|
92
|
+
scrollToBottom(first ? 'auto' : undefined);
|
|
93
|
+
else
|
|
94
|
+
setNewCount((value) => value + 1);
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [messages]);
|
|
97
|
+
// Po każdej zmianie listy wiersze stoją gdzie indziej — pigułkę trzeba przeliczyć, inaczej
|
|
98
|
+
// pokazywałaby dzień sprzed dojścia nowej wiadomości.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
syncDay();
|
|
101
|
+
}, [messages, offline, activeLocale, syncDay]);
|
|
102
|
+
// To samo po zmianie szerokości: zawinięty tekst przesuwa wiersze spod pigułki.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const element = viewportRef.current;
|
|
105
|
+
if (!element || !globalThis.ResizeObserver)
|
|
106
|
+
return;
|
|
107
|
+
const observer = new ResizeObserver(() => syncDay());
|
|
108
|
+
observer.observe(element);
|
|
109
|
+
return () => observer.disconnect();
|
|
110
|
+
}, [syncDay]);
|
|
111
|
+
const onScroll = (element) => {
|
|
112
|
+
const distance = element.scrollHeight - element.scrollTop - element.clientHeight;
|
|
113
|
+
const atBottom = distance < PIN_THRESHOLD;
|
|
114
|
+
setPinned(atBottom);
|
|
115
|
+
if (atBottom)
|
|
116
|
+
setNewCount(0);
|
|
117
|
+
// `onLoadMore` tylko na wejściu w strefę szczytu — przy każdym zdarzeniu scroll
|
|
118
|
+
// aplikacja dostawałaby dziesiątki żądań na jedno przewinięcie.
|
|
119
|
+
const atTop = element.scrollTop < PIN_THRESHOLD;
|
|
120
|
+
if (atTop && !wasAtTop.current)
|
|
121
|
+
onLoadMore?.();
|
|
122
|
+
wasAtTop.current = atTop;
|
|
123
|
+
syncDay();
|
|
124
|
+
};
|
|
125
|
+
const jumpToLatest = () => {
|
|
126
|
+
setPinned(true);
|
|
127
|
+
setNewCount(0);
|
|
128
|
+
scrollToBottom();
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Etykieta separatora dnia. Rdzeń zwraca rodzaj, słownik daje „dziś"/„wczoraj",
|
|
132
|
+
* a nazwę dnia i pełną datę — `Intl`, żeby nie trzymać w słowniku kalendarza.
|
|
133
|
+
*/
|
|
134
|
+
const dateText = (label) => {
|
|
135
|
+
switch (label.kind) {
|
|
136
|
+
case 'today':
|
|
137
|
+
return t('chat.today');
|
|
138
|
+
case 'yesterday':
|
|
139
|
+
return t('chat.yesterday');
|
|
140
|
+
case 'weekday':
|
|
141
|
+
return new Intl.DateTimeFormat(activeLocale, { weekday: 'long' }).format(label.date);
|
|
142
|
+
default:
|
|
143
|
+
return new Intl.DateTimeFormat(activeLocale, {
|
|
144
|
+
day: 'numeric',
|
|
145
|
+
month: 'long',
|
|
146
|
+
year: 'numeric',
|
|
147
|
+
}).format(label.date);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
return (_jsxs("div", { className: cn('kpt-chat-host', className), ...rest, children: [offline ? (_jsxs("p", { className: "kpt-chat__offline", role: "status", children: [_jsx(KptIcon, { name: "alert-triangle" }), _jsx("span", { children: t('chat.offline') }), pending ? (_jsx("span", { className: "kpt-chat__offline-count", children: t('chat.pending', { count: pending }) })) : null] })) : null, _jsxs("div", { className: "kpt-chat__body", children: [_jsxs("div", { ref: viewportRef, className: "kpt-chat__viewport", role: "log", "aria-live": "polite", "aria-relevant": "additions", onScroll: (event) => onScroll(event.currentTarget), children: [rows.length === 0 ? (_jsx("p", { className: "kpt-chat__empty", children: emptyLabel || t('chat.empty') })) : null, rows.map((row) => {
|
|
151
|
+
if (row.kind === 'date') {
|
|
152
|
+
const label = dateText(row.label);
|
|
153
|
+
return (_jsx("p", { className: "kpt-chat__date", role: "separator", "aria-label": label, children: label }, row.key));
|
|
154
|
+
}
|
|
155
|
+
if (row.kind === 'unread') {
|
|
156
|
+
return (_jsx("p", { className: "kpt-chat__unread", role: "separator", "aria-label": t('chat.newMessages', { count: row.count }), children: _jsx("span", { children: t('chat.unread') }) }, row.key));
|
|
157
|
+
}
|
|
158
|
+
return (_jsx(KptChatMessage, { message: row.message, text: renderMessage ? '' : null, align: row.own ? 'end' : 'start', position: row.position, showAuthor: row.showAuthor, showAvatar: row.showAvatar, showMeta: row.showMeta, actions: actions, reactionOptions: reactionOptions, locale: locale, dictionary: dictionary, onRetry: () => onRetry?.(row.message), onReply: () => onReply?.(row.message), onReact: (emoji) => onReact?.({ message: row.message, emoji }), onQuoted: (quote) => onQuoted?.(quote), children: renderMessage ? renderMessage(row.message, row) : null }, row.key));
|
|
159
|
+
}), typing.length ? _jsx(KptChatTyping, { authors: typing, locale: locale, dictionary: dictionary }) : null] }), _jsx("p", { ref: dayRef, className: "kpt-chat__day", "aria-hidden": "true", "data-hidden": dayVisible ? undefined : '', children: dayLabel }), newCount ? (_jsxs("button", { type: "button", className: "kpt-chat__jump", onClick: jumpToLatest, children: [_jsx(KptIcon, { name: "arrow-down" }), t('chat.newMessages', { count: newCount })] })) : null] })] }));
|
|
160
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# KptChat (@konce-pt/react/chat)
|
|
2
|
+
|
|
3
|
+
A conversation list: grouped messages, day separators, an unread divider, the view pinned to the
|
|
4
|
+
newest message and an offline strip counting what still has to go out.
|
|
5
|
+
Import: `import { KptChat } from '@konce-pt/react/chat';`
|
|
6
|
+
|
|
7
|
+
## DOM
|
|
8
|
+
<div class="kpt-chat-host">
|
|
9
|
+
<p class="kpt-chat__offline" role="status">…</p>
|
|
10
|
+
<div class="kpt-chat__body">
|
|
11
|
+
<div class="kpt-chat__viewport" role="log" aria-live="polite">
|
|
12
|
+
<p class="kpt-chat__date" role="separator">Today</p>
|
|
13
|
+
<div class="kpt-chat-message-host">…</div>
|
|
14
|
+
<p class="kpt-chat__unread" role="separator">…</p>
|
|
15
|
+
<div class="kpt-chat-typing-host">…</div>
|
|
16
|
+
</div>
|
|
17
|
+
<p class="kpt-chat__day" aria-hidden="true">Today</p>
|
|
18
|
+
<button class="kpt-chat__jump">3 new messages</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
## Props
|
|
23
|
+
- `messages`: `KptChatMessage<T>[]` — **in chronological order**; the component does not sort
|
|
24
|
+
- `currentUser`: string — the author id treated as "me"
|
|
25
|
+
- `groupGapMs`: number (default `KPT_CHAT_GROUP_GAP_MS`, 5 min)
|
|
26
|
+
- `unreadFrom`: string | null — the id of the first unread message
|
|
27
|
+
- `offline`: boolean, `typing`: `KptChatAuthor[]`, `autoScroll`: boolean (default true)
|
|
28
|
+
- `actions`: boolean — reply and react buttons on hover; `emptyLabel`: string
|
|
29
|
+
- `reactionOptions`: `readonly KptChatReactionOption[]` (default `KPT_CHAT_REACTIONS`) — passed to the bubbles
|
|
30
|
+
- `onRetry` / `onReply` — `(message) => void`; `onReact` — `(event: KptChatReactEvent) => void`
|
|
31
|
+
(`{ message, emoji }`); `onQuoted` — `(quote) => void`
|
|
32
|
+
- `onLoadMore` — fired once when the viewport reaches the top, not on every scroll event
|
|
33
|
+
- `renderMessage(message, row)` — the equivalent of `<ng-template let-message>`; it replaces the
|
|
34
|
+
bubble's text, and the bubble, its grouping and its meta row stay in place
|
|
35
|
+
- `className`, `ref` and every other `<div>` prop go to the host
|
|
36
|
+
|
|
37
|
+
## The composer is a sibling, not a slot
|
|
38
|
+
A read-only conversation (an archive, a preview) should not have to carry a text field it never
|
|
39
|
+
shows, so `KptChatComposer` is placed next to the list rather than inside it. The parent is what
|
|
40
|
+
gives the list its height — `.kpt-chat-host` is a column flexbox with `min-height: 0`.
|
|
41
|
+
|
|
42
|
+
## Transport stays in the application
|
|
43
|
+
The component takes messages and only reports. Retrying a send is a status change the application
|
|
44
|
+
makes; `mergeStatus()` from `@konce-pt/chat` is there for out-of-order acks.
|
|
45
|
+
|
|
46
|
+
## Bubble actions: reply and react
|
|
47
|
+
`actions` puts two buttons beside the bubble. The arrow emits `onReply` with the message —
|
|
48
|
+
`replyRefFrom()` from `@konce-pt/chat` turns it into the `replyTo` the composer's quote bar wants.
|
|
49
|
+
The smiley opens a picker of five reactions (`KPT_CHAT_REACTIONS`) and emits `onReact` with the
|
|
50
|
+
chosen emoji; `toggleReaction(message.reactions, emoji)` is the arithmetic that puts it in the
|
|
51
|
+
model. The library writes nothing itself.
|
|
52
|
+
|
|
53
|
+
## One day pill, not a stack of them
|
|
54
|
+
The separators in the list travel with the content; which day the view is standing in is told by one
|
|
55
|
+
pill floating over the list (`.kpt-chat__day`, `aria-hidden`). On every scroll (and after every
|
|
56
|
+
re-render and resize) the component measures the separators, the unread divider and the system
|
|
57
|
+
notices, hands the rectangles to `chatDayIndicator()` from `@konce-pt/chat` and swaps the pill's
|
|
58
|
+
text; a centred label driving into the pill's band puts the pill out, never the content.
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
<KptChat messages={messages} currentUser="me" typing={typing} onRetry={resend} />
|
|
62
|
+
<KptChat messages={messages} currentUser="me" offline unreadFrom="m-42" onLoadMore={loadOlder} />
|
|
63
|
+
<KptChat messages={messages} currentUser="me" renderMessage={(m) => <KptCard>{m.data.title}</KptCard>} />
|
|
64
|
+
|
|
65
|
+
## i18n
|
|
66
|
+
`today`, `yesterday`, `unread`, `newMessages` (`{count}`), `empty`, `offline`, `pending`
|
|
67
|
+
(`{count}`) from the `chat` namespace. Weekday names and full dates come from `Intl`, not the
|
|
68
|
+
dictionary.
|
|
69
|
+
|
|
70
|
+
## Tokens
|
|
71
|
+
The offline strip `--kpt-color-warning-subtle`, the day separator pill `--kpt-color-surface-raised`
|
|
72
|
+
+ `--kpt-elevation-1`, the jump
|
|
73
|
+
pill `--kpt-color-primary-border` + `--kpt-elevation-2`.
|
|
74
|
+
|
|
75
|
+
## Accessibility
|
|
76
|
+
`role="log"` with `aria-live="polite"` announces a new message without cutting off what is being
|
|
77
|
+
read. Separators are `role="separator"` with a label. Auto-scroll uses `behavior: 'auto'` under
|
|
78
|
+
`prefers-reduced-motion`.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef } from 'react';
|
|
2
|
+
import type { KptChatReplyRef } from '@konce-pt/chat';
|
|
3
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
4
|
+
/** Ładunek wysyłki. Kompozytor niczego nie wysyła — pakuje dane i oddaje aplikacji. */
|
|
5
|
+
export interface KptChatComposerSubmit {
|
|
6
|
+
text: string;
|
|
7
|
+
files: readonly File[];
|
|
8
|
+
replyTo: KptChatReplyRef | null;
|
|
9
|
+
}
|
|
10
|
+
export interface KptChatComposerProps extends Omit<ComponentPropsWithRef<'div'>, 'children' | 'onSubmit'>, KptI18nOverrides {
|
|
11
|
+
value?: string;
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
onValueChange?: (value: string) => void;
|
|
14
|
+
/** Podpowiedź w pustym polu; bez niej etykieta ze słownika. */
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Brak połączenia — przycisk mówi o kolejkowaniu, nie o wysyłce. */
|
|
18
|
+
offline?: boolean;
|
|
19
|
+
allowAttachments?: boolean;
|
|
20
|
+
/** Ile wierszy pole może urosnąć, zanim zacznie się przewijać. */
|
|
21
|
+
maxRows?: number;
|
|
22
|
+
enterToSend?: boolean;
|
|
23
|
+
/** Wiadomość, na którą odpowiadamy. Po wysłaniu czyści się razem z treścią. */
|
|
24
|
+
replyTo?: KptChatReplyRef | null;
|
|
25
|
+
defaultReplyTo?: KptChatReplyRef | null;
|
|
26
|
+
onReplyToChange?: (replyTo: KptChatReplyRef | null) => void;
|
|
27
|
+
onSend?: (payload: KptChatComposerSubmit) => void;
|
|
28
|
+
/** Uderzenie w klawiaturę — sygnał „piszę" dla drugiej strony. */
|
|
29
|
+
onTyping?: () => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pole pisania wiadomości — autorosnące, z załącznikami i paskiem cytatu.
|
|
33
|
+
*
|
|
34
|
+
* `Enter` wysyła, `Shift+Enter` łamie linię (`enterToSend={false}` odwraca układ).
|
|
35
|
+
* Przy `offline` przycisk zmienia etykietę na kolejkowanie — wiadomość dalej da się napisać
|
|
36
|
+
* i oddać aplikacji, bo to ona decyduje, co zrobić z kolejką.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* <KptChatComposer allowAttachments onSend={(payload) => send(payload)} />
|
|
40
|
+
*/
|
|
41
|
+
export declare function KptChatComposer({ value, defaultValue, onValueChange, placeholder, disabled, offline, allowAttachments, maxRows, enterToSend, replyTo, defaultReplyTo, onReplyToChange, onSend, onTyping, locale, dictionary, className, ...rest }: Readonly<KptChatComposerProps>): import("react").JSX.Element;
|