@konce-pt/react 0.8.2 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +11 -4
  2. package/dist/backdrop/backdrop.d.ts +79 -0
  3. package/dist/backdrop/backdrop.js +195 -0
  4. package/dist/backdrop/llms.txt +132 -0
  5. package/dist/chat/chat.d.ts +53 -0
  6. package/dist/chat/chat.js +160 -0
  7. package/dist/chat/llms.txt +78 -0
  8. package/dist/chat-composer/chat-composer.d.ts +41 -0
  9. package/dist/chat-composer/chat-composer.js +81 -0
  10. package/dist/chat-composer/llms.txt +56 -0
  11. package/dist/chat-dock/chat-dock.d.ts +40 -0
  12. package/dist/chat-dock/chat-dock.js +67 -0
  13. package/dist/chat-dock/llms.txt +71 -0
  14. package/dist/chat-entry/public-api.d.ts +12 -0
  15. package/dist/chat-entry/public-api.js +10 -0
  16. package/dist/chat-message/chat-message.d.ts +53 -0
  17. package/dist/chat-message/chat-message.js +112 -0
  18. package/dist/chat-message/llms.txt +73 -0
  19. package/dist/chat-typing/chat-typing.d.ts +23 -0
  20. package/dist/chat-typing/chat-typing.js +30 -0
  21. package/dist/chat-typing/llms.txt +41 -0
  22. package/dist/chip/chip.d.ts +10 -1
  23. package/dist/chip/chip.js +3 -2
  24. package/dist/chip/llms.txt +9 -0
  25. package/dist/data-view/data-view.d.ts +15 -3
  26. package/dist/data-view/data-view.js +22 -5
  27. package/dist/data-view/llms.txt +20 -7
  28. package/dist/internal/dev.d.ts +1 -0
  29. package/dist/internal/dev.js +3 -0
  30. package/dist/internal/overlay.d.ts +6 -1
  31. package/dist/internal/overlay.js +5 -3
  32. package/dist/mention/llms.txt +60 -0
  33. package/dist/mention/mention.d.ts +38 -0
  34. package/dist/mention/mention.js +29 -0
  35. package/dist/mention/mentions.d.ts +31 -0
  36. package/dist/mention/mentions.js +24 -0
  37. package/dist/order-list/llms.txt +16 -5
  38. package/dist/order-list/order-list.d.ts +5 -3
  39. package/dist/order-list/order-list.js +69 -4
  40. package/dist/peek/llms.txt +42 -0
  41. package/dist/peek/peek.d.ts +35 -0
  42. package/dist/peek/peek.js +127 -0
  43. package/dist/pick-list/llms.txt +19 -5
  44. package/dist/pick-list/pick-list.d.ts +7 -2
  45. package/dist/pick-list/pick-list.js +123 -17
  46. package/dist/public-api.d.ts +13 -1
  47. package/dist/public-api.js +6 -0
  48. package/dist/rich-text/llms.txt +10 -0
  49. package/dist/rich-text/rich-text.d.ts +11 -1
  50. package/dist/rich-text/rich-text.js +157 -5
  51. package/dist/rich-text/sanitize.js +10 -1
  52. package/dist/roadmap/roadmap.js +1 -3
  53. package/package.json +16 -9
@@ -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;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useState, } from 'react';
3
+ import { cn } from "../internal/cn.js";
4
+ import { useControllableState } from "../internal/use-controllable-state.js";
5
+ import { useKptMessages } from "../i18n/i18n-context.js";
6
+ import { KptIcon } from "../icon/icon.js";
7
+ import { KptIconButton } from "../icon-button/icon-button.js";
8
+ /**
9
+ * Pole pisania wiadomości — autorosnące, z załącznikami i paskiem cytatu.
10
+ *
11
+ * `Enter` wysyła, `Shift+Enter` łamie linię (`enterToSend={false}` odwraca układ).
12
+ * Przy `offline` przycisk zmienia etykietę na kolejkowanie — wiadomość dalej da się napisać
13
+ * i oddać aplikacji, bo to ona decyduje, co zrobić z kolejką.
14
+ *
15
+ * @example
16
+ * <KptChatComposer allowAttachments onSend={(payload) => send(payload)} />
17
+ */
18
+ export function KptChatComposer({ value, defaultValue = '', onValueChange, placeholder = '', disabled = false, offline = false, allowAttachments = false, maxRows = 6, enterToSend = true, replyTo, defaultReplyTo = null, onReplyToChange, onSend, onTyping, locale, dictionary, className, ...rest }) {
19
+ const { t } = useKptMessages({ locale, dictionary });
20
+ const [text, setText] = useControllableState({
21
+ value,
22
+ defaultValue,
23
+ onChange: onValueChange,
24
+ });
25
+ const [quote, setQuote] = useControllableState({
26
+ value: replyTo,
27
+ defaultValue: defaultReplyTo,
28
+ onChange: onReplyToChange,
29
+ });
30
+ const [files, setFiles] = useState([]);
31
+ const fieldRef = useRef(null);
32
+ const pickerRef = useRef(null);
33
+ const canSend = !disabled && (text.trim().length > 0 || files.length > 0);
34
+ const sendLabel = offline ? t('chat.queue') : t('chat.send');
35
+ /**
36
+ * Wysokość liczona od treści. Zerowanie przed odczytem `scrollHeight` jest konieczne:
37
+ * bez niego pole potrafi tylko rosnąć, bo `scrollHeight` nigdy nie zejdzie poniżej
38
+ * ustawionej wcześniej wysokości. Górny limit trzyma CSS (`--_max-rows`).
39
+ */
40
+ const grow = (element) => {
41
+ element.style.height = 'auto';
42
+ // `scrollHeight` liczy treść z paddingiem, ale BEZ ramki, a pole jest `border-box`.
43
+ // Sama wysokość ze `scrollHeight` zostawia je o grubość ramki za niskie, więc puste
44
+ // pole na jeden wiersz dostaje pasek przewijania i nigdy się go nie pozbywa.
45
+ const borders = element.offsetHeight - element.clientHeight;
46
+ element.style.height = element.scrollHeight + borders + 'px';
47
+ };
48
+ const submit = () => {
49
+ if (!canSend)
50
+ return;
51
+ onSend?.({ text: text.trim(), files, replyTo: quote });
52
+ setText('');
53
+ setFiles([]);
54
+ setQuote(null);
55
+ const field = fieldRef.current;
56
+ if (field) {
57
+ field.value = '';
58
+ grow(field);
59
+ }
60
+ };
61
+ const onKeyDown = (event) => {
62
+ if (event.key !== 'Enter')
63
+ return;
64
+ const wantsSend = enterToSend ? !event.shiftKey : event.shiftKey;
65
+ if (!wantsSend || event.nativeEvent.isComposing)
66
+ return;
67
+ event.preventDefault();
68
+ submit();
69
+ };
70
+ return (_jsxs("div", { className: cn('kpt-chat-composer-host', className), ...rest, children: [quote ? (_jsxs("div", { className: "kpt-chat-composer__reply", children: [_jsxs("span", { className: "kpt-chat-composer__reply-body", children: [_jsx("span", { className: "kpt-chat-composer__reply-author", children: t('chat.replyTo', { author: quote.author }) }), _jsx("span", { className: "kpt-chat-composer__reply-text", children: quote.excerpt })] }), _jsx(KptIconButton, { size: "sm", "aria-label": t('chat.cancelReply'), onClick: () => setQuote(null), children: _jsx(KptIcon, { name: "x" }) })] })) : null, files.length ? (_jsx("ul", { className: "kpt-chat-composer__files", children: files.map((file) => (_jsxs("li", { className: "kpt-chat-composer__file", children: [_jsx(KptIcon, { name: "paperclip" }), _jsx("span", { className: "kpt-chat-composer__file-name", children: file.name }), _jsx(KptIconButton, { size: "sm", "aria-label": t('chat.removeAttachment'), onClick: () => setFiles((current) => current.filter((item) => item !== file)), children: _jsx(KptIcon, { name: "x" }) })] }, file.name + file.size + file.lastModified))) })) : null, _jsxs("div", { className: "kpt-chat-composer", children: [allowAttachments ? (_jsxs(_Fragment, { children: [_jsx("input", { ref: pickerRef, type: "file", className: "kpt-chat-composer__picker", multiple: true, onChange: (event) => {
71
+ const picked = Array.from(event.currentTarget.files ?? []);
72
+ if (picked.length)
73
+ setFiles((current) => [...current, ...picked]);
74
+ // Bez wyzerowania ten sam plik wybrany drugi raz nie wywoła `change`.
75
+ event.currentTarget.value = '';
76
+ } }), _jsx(KptIconButton, { size: "sm", disabled: disabled, "aria-label": t('chat.attach'), onClick: () => pickerRef.current?.click(), children: _jsx(KptIcon, { name: "paperclip" }) })] })) : null, _jsx("textarea", { ref: fieldRef, className: "kpt-chat-composer__field", rows: 1, style: { '--_max-rows': maxRows }, value: text, disabled: disabled, placeholder: placeholder || t('chat.placeholder'), "aria-label": placeholder || t('chat.placeholder'), onChange: (event) => {
77
+ setText(event.currentTarget.value);
78
+ grow(event.currentTarget);
79
+ onTyping?.();
80
+ }, onKeyDown: onKeyDown }), _jsx(KptIconButton, { variant: "filled", disabled: !canSend, "aria-label": sendLabel, onClick: submit, children: _jsx(KptIcon, { name: offline ? 'clock' : 'send' }) })] })] }));
81
+ }
@@ -0,0 +1,56 @@
1
+ # KptChatComposer (@konce-pt/react/chat)
2
+
3
+ The message field — auto-growing, with attachments and a quote strip.
4
+ Import: `import { KptChatComposer } from '@konce-pt/react/chat';`
5
+
6
+ ## DOM
7
+ <div class="kpt-chat-composer-host">
8
+ <div class="kpt-chat-composer__reply">…the quoted message…</div>
9
+ <ul class="kpt-chat-composer__files">…picked files…</ul>
10
+ <div class="kpt-chat-composer">
11
+ <input type="file" class="kpt-chat-composer__picker" hidden />
12
+ <!-- KptIconButton: attach -->
13
+ <textarea class="kpt-chat-composer__field" rows="1"></textarea>
14
+ <!-- KptIconButton: send -->
15
+ </div>
16
+ </div>
17
+
18
+ ## Props
19
+ - `value` / `defaultValue` / `onValueChange` — the text
20
+ - `replyTo` / `defaultReplyTo` / `onReplyToChange` — `KptChatReplyRef | null`
21
+ - `placeholder`, `disabled`, `offline`, `allowAttachments`
22
+ - `maxRows`: number (default 6), `enterToSend`: boolean (default true)
23
+ - `onSend(payload)` — `{ text, files, replyTo }`; the text is trimmed
24
+ - `onTyping()` — every keystroke, for the "is typing" signal
25
+ - `className`, `ref` and every other `<div>` prop go to the host
26
+
27
+ ## Enter sends, Shift+Enter breaks the line
28
+ `enterToSend={false}` swaps the two. A keystroke arriving mid-composition (an IME) never sends —
29
+ `isComposing` is checked, otherwise picking a candidate would fire the message.
30
+
31
+ ## The height comes from the content
32
+ The field grows with the text and then scrolls. The script sets `height` to `scrollHeight`, having
33
+ zeroed it first — without that reset the field can only grow, because `scrollHeight` never drops
34
+ below the height already set. The ceiling is CSS: `--_max-rows`.
35
+
36
+ ## Offline does not lock the field
37
+ The button changes its label to queueing and the payload still reaches `onSend`. What to do with a
38
+ queue is the application's decision — the component has no transport to hold it back with.
39
+
40
+ ## Examples
41
+ <KptChatComposer allowAttachments onSend={send} />
42
+ <KptChatComposer value={draft} onValueChange={setDraft} offline={!online} onSend={queue} />
43
+ <KptChatComposer replyTo={quote} onReplyToChange={setQuote} enterToSend={false} onSend={send} />
44
+
45
+ ## i18n
46
+ `placeholder`, `send`, `queue`, `attach`, `removeAttachment`, `replyTo` (`{author}`),
47
+ `cancelReply` from the `chat` namespace.
48
+
49
+ ## Tokens
50
+ The field `--kpt-color-surface-variant` + `--kpt-radius-lg`, the top edge `--kpt-color-border`,
51
+ the quote strip `--kpt-color-primary`.
52
+
53
+ ## Accessibility
54
+ The textarea carries an `aria-label` equal to the placeholder — a field without a label is
55
+ invisible to a screen reader. Both icon buttons are labelled from the dictionary. It is not a
56
+ form control: no `touched`/`invalid` state, and the value disappears on send.
@@ -0,0 +1,40 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
+ export type KptChatDockPosition = 'bottom-end' | 'bottom-start';
4
+ export interface KptChatDockProps extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
5
+ open?: boolean;
6
+ defaultOpen?: boolean;
7
+ onOpenChange?: (open: boolean) => void;
8
+ position?: KptChatDockPosition;
9
+ /** Licznik na plakietce FAB-a. Zero chowa plakietkę. */
10
+ unread?: number;
11
+ header?: string;
12
+ /** Ikona zamkniętego FAB-a; otwarty zawsze pokazuje krzyżyk. */
13
+ icon?: string;
14
+ /** Szerokość panelu na ekranach powyżej `--kpt-breakpoint-sm`. */
15
+ width?: string;
16
+ /**
17
+ * Odsunięcie od rogu ekranu (dowolna długość CSS), wspólne dla obu osi.
18
+ * Domyślnie `--kpt-space-6`. Przydaje się, gdy w tym samym rogu stoi już inny widget —
19
+ * `KptScrollTop` przykleja się dokładnie tam samo i bez tego oba się nakładają.
20
+ */
21
+ offset?: string;
22
+ children?: ReactNode;
23
+ }
24
+ /**
25
+ * Pływający czat w rogu ekranu — FAB z licznikiem nieprzeczytanych i panel z rozmową.
26
+ *
27
+ * Zawartość panelu jest dzieckiem, więc dock nie wie nic o wiadomościach: wkłada się do niego
28
+ * `KptChat` i `KptChatComposer` (albo cokolwiek innego). Sam FAB to `KptFab` — dock go używa,
29
+ * nie zastępuje.
30
+ *
31
+ * Poniżej `--kpt-breakpoint-sm` panel wchodzi na pełny ekran: 22,5 rem szerokości na telefonie
32
+ * to nie panel, tylko szpara.
33
+ *
34
+ * @example
35
+ * <KptChatDock unread={3} header="Wsparcie">
36
+ * <KptChat messages={messages} currentUser="me" />
37
+ * <KptChatComposer onSend={send} />
38
+ * </KptChatDock>
39
+ */
40
+ export declare function KptChatDock({ open, defaultOpen, onOpenChange, position, unread, header, icon, width, offset, locale, dictionary, className, children, ...rest }: Readonly<KptChatDockProps>): import("react").JSX.Element;
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, } from 'react';
3
+ import { cn } from "../internal/cn.js";
4
+ import { useControllableState } from "../internal/use-controllable-state.js";
5
+ import { useKptMessages } from "../i18n/i18n-context.js";
6
+ import { KptBadge } from "../badge/badge.js";
7
+ import { KptFab } from "../fab/fab.js";
8
+ import { KptIcon } from "../icon/icon.js";
9
+ import { KptIconButton } from "../icon-button/icon-button.js";
10
+ /**
11
+ * Pływający czat w rogu ekranu — FAB z licznikiem nieprzeczytanych i panel z rozmową.
12
+ *
13
+ * Zawartość panelu jest dzieckiem, więc dock nie wie nic o wiadomościach: wkłada się do niego
14
+ * `KptChat` i `KptChatComposer` (albo cokolwiek innego). Sam FAB to `KptFab` — dock go używa,
15
+ * nie zastępuje.
16
+ *
17
+ * Poniżej `--kpt-breakpoint-sm` panel wchodzi na pełny ekran: 22,5 rem szerokości na telefonie
18
+ * to nie panel, tylko szpara.
19
+ *
20
+ * @example
21
+ * <KptChatDock unread={3} header="Wsparcie">
22
+ * <KptChat messages={messages} currentUser="me" />
23
+ * <KptChatComposer onSend={send} />
24
+ * </KptChatDock>
25
+ */
26
+ export function KptChatDock({ open, defaultOpen = false, onOpenChange, position = 'bottom-end', unread = 0, header = '', icon = 'message-circle', width = '22.5rem', offset = '', locale, dictionary, className, children, ...rest }) {
27
+ const { t } = useKptMessages({ locale, dictionary });
28
+ const [isOpen, setOpen] = useControllableState({
29
+ value: open,
30
+ defaultValue: defaultOpen,
31
+ onChange: onOpenChange,
32
+ });
33
+ const panelRef = useRef(null);
34
+ const triggerRef = useRef(null);
35
+ /*
36
+ * Fokus ląduje na polu pisania, jeśli w panelu jest — po to się czat otwiera.
37
+ * Bez pola bierze pierwszy element interaktywny, a w ostateczności sam panel.
38
+ */
39
+ useEffect(() => {
40
+ if (!isOpen)
41
+ return;
42
+ const element = panelRef.current;
43
+ if (!element)
44
+ return;
45
+ const frame = requestAnimationFrame(() => {
46
+ const target = element.querySelector('textarea:not([disabled])') ??
47
+ element.querySelector('input:not([disabled]), button:not([disabled])') ??
48
+ element;
49
+ target.focus();
50
+ });
51
+ return () => cancelAnimationFrame(frame);
52
+ }, [isOpen]);
53
+ const close = () => {
54
+ if (!isOpen)
55
+ return;
56
+ setOpen(false);
57
+ // Fokus wraca na FAB — po zamknięciu panelu nie może zostać na elemencie,
58
+ // który przed chwilą zniknął z drzewa dostępności.
59
+ triggerRef.current?.querySelector('button')?.focus();
60
+ };
61
+ const onKeyDown = (event) => {
62
+ if (event.key === 'Escape')
63
+ close();
64
+ };
65
+ const fab = (_jsx(KptFab, { "aria-label": isOpen ? t('chat.close') : t('chat.open'), onClick: () => setOpen(!isOpen), children: _jsx(KptIcon, { name: isOpen ? 'x' : icon }) }));
66
+ return (_jsxs("div", { className: cn('kpt-chat-dock-host', className), onKeyDown: onKeyDown, ...rest, children: [_jsxs("div", { ref: panelRef, className: cn('kpt-chat-dock__panel', isOpen && 'is-open'), "data-position": position, role: "dialog", tabIndex: -1, style: { '--_width': width, ...(offset ? { '--_offset': offset } : {}) }, "aria-label": header || t('chat.open'), "aria-hidden": isOpen ? undefined : true, inert: !isOpen, children: [_jsxs("header", { className: "kpt-chat-dock__header", children: [_jsx("span", { className: "kpt-chat-dock__title", children: header }), _jsx(KptIconButton, { size: "sm", "aria-label": t('chat.close'), onClick: close, children: _jsx(KptIcon, { name: "x" }) })] }), _jsx("div", { className: "kpt-chat-dock__content", children: children })] }), _jsx("span", { ref: triggerRef, className: "kpt-chat-dock__trigger", "data-position": position, style: offset ? { '--_offset': offset } : undefined, children: unread > 0 ? (_jsx(KptBadge, { value: unread, variant: "danger", size: "sm", overlay: true, "aria-label": t('chat.unreadBadge', { count: unread }), children: fab })) : (fab) })] }));
67
+ }
@@ -0,0 +1,71 @@
1
+ # KptChatDock (@konce-pt/react/chat)
2
+
3
+ A floating chat in the corner of the screen — a FAB with an unread badge plus a panel holding the
4
+ conversation.
5
+ Import: `import { KptChatDock } from '@konce-pt/react/chat';`
6
+
7
+ ## DOM
8
+ <div class="kpt-chat-dock-host">
9
+ <div class="kpt-chat-dock__panel is-open" role="dialog" data-position="bottom-end">
10
+ <header class="kpt-chat-dock__header">
11
+ <span class="kpt-chat-dock__title">Support</span>
12
+ <!-- KptIconButton: close -->
13
+ </header>
14
+ <div class="kpt-chat-dock__content">…children…</div>
15
+ </div>
16
+ <span class="kpt-chat-dock__trigger" data-position="bottom-end">
17
+ <!-- KptBadge overlay → KptFab -->
18
+ </span>
19
+ </div>
20
+
21
+ ## Props
22
+ - `open` / `defaultOpen` / `onOpenChange` — works controlled and uncontrolled
23
+ - `position`: `'bottom-end' | 'bottom-start'` (logical, so RTL flips the corner on its own)
24
+ - `unread`: number — 0 hides the badge; `header`: string
25
+ - `icon`: string (default `message-circle`) — the closed FAB; open always shows a cross
26
+ - `width`: string (default `22.5rem`) — above `--kpt-breakpoint-sm`
27
+ - `offset`: string — distance from the corner, both axes (default `--kpt-space-6`)
28
+ - `children` — the panel content
29
+ - `className`, `ref` and every other `<div>` prop go to the host
30
+
31
+ ## The FAB is KptFab, not a copy of it
32
+ The dock composes the existing components — `KptFab`, `KptBadge`, `KptIconButton` — so a change to
33
+ the FAB reaches the chat as well. The content is `children`, which means the dock knows nothing
34
+ about messages: put `KptChat` and `KptChatComposer` inside, or anything else.
35
+
36
+ ## Sharing the corner with KptScrollTop
37
+ `KptScrollTop` pins itself to the same spot with the same `--kpt-z-overlay`, and it would swallow the
38
+ chat FAB whole — only the unread badge would stick out. The stylesheet resolves it by itself: when a
39
+ `bottom-end` dock is on the page, the scroll-top button steps **aside**, not up, because the
40
+ conversation panel stands above the FAB. It applies to `bottom-end` only and measures from the
41
+ default offset, so an application that sets `offset` positions the scroll-top button itself.
42
+
43
+ ## The host lays out nothing
44
+ `.kpt-chat-dock-host` is `display: contents`, and both the FAB and the panel are `position: fixed`.
45
+ Dropped in the middle of an application's layout the dock takes no space in the flow.
46
+
47
+ ## Closed is not removed
48
+ The panel stays in the DOM with `visibility: hidden` and a delayed transition — a transition
49
+ cannot animate out of `display: none`. It also carries `inert` and `aria-hidden` while closed, so
50
+ a hidden panel never catches the tab order.
51
+
52
+ ## Examples
53
+ <KptChatDock unread={3} header="Support">
54
+ <KptChat messages={messages} currentUser="me" />
55
+ <KptChatComposer onSend={send} />
56
+ </KptChatDock>
57
+
58
+ <KptChatDock open={open} onOpenChange={setOpen} position="bottom-start" width="26rem">…</KptChatDock>
59
+
60
+ ## i18n
61
+ `open`, `close`, `unreadBadge` (`{count}`) from the `chat` namespace.
62
+
63
+ ## Tokens
64
+ The panel `--kpt-radius-lg` + `--kpt-elevation-3`, the corner offsets `--kpt-space-6`,
65
+ the stack `--kpt-z-overlay`.
66
+
67
+ ## Accessibility
68
+ Opening moves focus to the composer inside the panel — that is what the chat is opened for; with no
69
+ field it takes the first interactive element. `Escape` closes and returns focus to the FAB, because
70
+ focus must not stay on an element that just left the accessibility tree. Below 640px the panel goes
71
+ full-screen.
@@ -0,0 +1,12 @@
1
+ export { KptChat } from '../chat/chat.tsx';
2
+ export type { KptChatProps, KptChatReactEvent } from '../chat/chat.tsx';
3
+ export { KptChatMessage } from '../chat-message/chat-message.tsx';
4
+ export type { KptChatMessageProps, KptChatMessageVariant } from '../chat-message/chat-message.tsx';
5
+ export { KptChatTyping } from '../chat-typing/chat-typing.tsx';
6
+ export type { KptChatTypingProps } from '../chat-typing/chat-typing.tsx';
7
+ export { KptChatComposer } from '../chat-composer/chat-composer.tsx';
8
+ export type { KptChatComposerProps, KptChatComposerSubmit } from '../chat-composer/chat-composer.tsx';
9
+ export { KptChatDock } from '../chat-dock/chat-dock.tsx';
10
+ export type { KptChatDockProps, KptChatDockPosition } from '../chat-dock/chat-dock.tsx';
11
+ export type { KptChatAttachment, KptChatAuthor, KptChatDateLabel, KptChatDateRow, KptChatGroupOptions, KptChatGroupPosition, KptChatMessage as KptChatMessageModel, KptChatMessageRow, KptChatReaction, KptChatReactionKind, KptChatReactionOption, KptChatReplyRef, KptChatRow, KptChatStatus, KptChatUnreadRow, } from '@konce-pt/chat';
12
+ export { KPT_CHAT_GROUP_GAP_MS, KPT_CHAT_REACTIONS, buildChatRows, canRetry, chatDayIndicator, dateLabel, diffInDays, isSameDay, mergeStatus, pendingCount, replyRefFrom, startOfDay, statusRank, toDate, toggleReaction, } from '@konce-pt/chat';
@@ -0,0 +1,10 @@
1
+ /*
2
+ * @konce-pt/react/chat — rozmowa. Osobny subpath, bo dock i logika listy są ciężkie
3
+ * względem rdzenia biblioteki i nie każda aplikacja ich potrzebuje.
4
+ */
5
+ export { KptChat } from "../chat/chat.js";
6
+ export { KptChatMessage } from "../chat-message/chat-message.js";
7
+ export { KptChatTyping } from "../chat-typing/chat-typing.js";
8
+ export { KptChatComposer } from "../chat-composer/chat-composer.js";
9
+ export { KptChatDock } from "../chat-dock/chat-dock.js";
10
+ export { KPT_CHAT_GROUP_GAP_MS, KPT_CHAT_REACTIONS, buildChatRows, canRetry, chatDayIndicator, dateLabel, diffInDays, isSameDay, mergeStatus, pendingCount, replyRefFrom, startOfDay, statusRank, toDate, toggleReaction, } from '@konce-pt/chat';