@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.
Files changed (65) hide show
  1. package/README.md +4 -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/button/button.d.ts +23 -7
  6. package/dist/button/button.js +33 -9
  7. package/dist/button/llms.txt +48 -12
  8. package/dist/button-group/button-group.d.ts +7 -1
  9. package/dist/button-group/button-group.js +9 -4
  10. package/dist/button-group/llms.txt +30 -5
  11. package/dist/chat/chat.d.ts +53 -0
  12. package/dist/chat/chat.js +160 -0
  13. package/dist/chat/llms.txt +78 -0
  14. package/dist/chat-composer/chat-composer.d.ts +41 -0
  15. package/dist/chat-composer/chat-composer.js +81 -0
  16. package/dist/chat-composer/llms.txt +56 -0
  17. package/dist/chat-dock/chat-dock.d.ts +40 -0
  18. package/dist/chat-dock/chat-dock.js +67 -0
  19. package/dist/chat-dock/llms.txt +71 -0
  20. package/dist/chat-entry/public-api.d.ts +12 -0
  21. package/dist/chat-entry/public-api.js +10 -0
  22. package/dist/chat-message/chat-message.d.ts +53 -0
  23. package/dist/chat-message/chat-message.js +112 -0
  24. package/dist/chat-message/llms.txt +73 -0
  25. package/dist/chat-typing/chat-typing.d.ts +23 -0
  26. package/dist/chat-typing/chat-typing.js +30 -0
  27. package/dist/chat-typing/llms.txt +41 -0
  28. package/dist/chip/chip.d.ts +10 -1
  29. package/dist/chip/chip.js +3 -2
  30. package/dist/chip/llms.txt +9 -0
  31. package/dist/data-view/data-view.d.ts +15 -3
  32. package/dist/data-view/data-view.js +22 -5
  33. package/dist/data-view/llms.txt +20 -7
  34. package/dist/dialog/dialog.d.ts +9 -1
  35. package/dist/dialog/dialog.js +2 -2
  36. package/dist/internal/dev.d.ts +1 -0
  37. package/dist/internal/dev.js +3 -0
  38. package/dist/internal/overlay.d.ts +6 -1
  39. package/dist/internal/overlay.js +5 -3
  40. package/dist/mention/llms.txt +60 -0
  41. package/dist/mention/mention.d.ts +38 -0
  42. package/dist/mention/mention.js +29 -0
  43. package/dist/mention/mentions.d.ts +31 -0
  44. package/dist/mention/mentions.js +24 -0
  45. package/dist/order-list/llms.txt +16 -5
  46. package/dist/order-list/order-list.d.ts +5 -3
  47. package/dist/order-list/order-list.js +69 -4
  48. package/dist/peek/llms.txt +42 -0
  49. package/dist/peek/peek.d.ts +35 -0
  50. package/dist/peek/peek.js +127 -0
  51. package/dist/pick-list/llms.txt +19 -5
  52. package/dist/pick-list/pick-list.d.ts +7 -2
  53. package/dist/pick-list/pick-list.js +123 -17
  54. package/dist/progress/llms.txt +70 -22
  55. package/dist/progress/progress.d.ts +34 -7
  56. package/dist/progress/progress.js +46 -9
  57. package/dist/public-api.d.ts +15 -3
  58. package/dist/public-api.js +6 -0
  59. package/dist/rich-text/llms.txt +10 -0
  60. package/dist/rich-text/rich-text.d.ts +11 -1
  61. package/dist/rich-text/rich-text.js +157 -5
  62. package/dist/rich-text/sanitize.js +10 -1
  63. package/dist/roadmap/roadmap.js +1 -3
  64. package/dist/shared/types.d.ts +2 -0
  65. package/package.json +16 -9
@@ -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';
@@ -0,0 +1,53 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import type { KptChatAttachment, KptChatAuthor, KptChatGroupPosition, KptChatMessage as KptChatMessageModel, KptChatReaction, KptChatReactionOption, KptChatReplyRef, KptChatStatus } from '@konce-pt/chat';
3
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
4
+ /** Wygląd bąbla. `system` nie rysuje bąbla w ogóle — to komunikat, nie wypowiedź. */
5
+ export type KptChatMessageVariant = 'neutral' | 'primary' | 'solid' | 'ghost' | 'system';
6
+ export interface KptChatMessageProps extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
7
+ /** Cała wiadomość naraz. Pojedyncze propsy poniżej mają nad nią priorytet. */
8
+ message?: KptChatMessageModel | null;
9
+ author?: KptChatAuthor | null;
10
+ text?: string | null;
11
+ time?: Date | string | number | null;
12
+ attachments?: readonly KptChatAttachment[] | null;
13
+ reactions?: readonly KptChatReaction[] | null;
14
+ replyTo?: KptChatReplyRef | null;
15
+ status?: KptChatStatus | null;
16
+ /** Strona listy: `start` to cudza wiadomość, `end` własna. */
17
+ align?: 'start' | 'end';
18
+ /** Wariant kolorystyczny; bez niego wynika z `align` (własna = `primary`). */
19
+ variant?: KptChatMessageVariant | null;
20
+ position?: KptChatGroupPosition;
21
+ system?: boolean;
22
+ showAuthor?: boolean;
23
+ showAvatar?: boolean;
24
+ showMeta?: boolean;
25
+ tail?: boolean;
26
+ /** Akcje pod kursorem: odpowiedz i zareaguj. Domyślnie wyłączone. */
27
+ actions?: boolean;
28
+ /** Zestaw wybieraka reakcji. Domyślnie pięć wbudowanych; można go skrócić lub przestawić. */
29
+ reactionOptions?: readonly KptChatReactionOption[];
30
+ onRetry?: () => void;
31
+ onReply?: () => void;
32
+ /** Wybrana reakcja (emoji). Biblioteka niczego nie dopisuje do modelu — robi to aplikacja. */
33
+ onReact?: (emoji: string) => void;
34
+ /** Kliknięcie w cytat — aplikacja może przewinąć listę do oryginału. */
35
+ onQuoted?: (quote: KptChatReplyRef) => void;
36
+ /** Treść bąbla. Wygrywa z `text`, więc bąbel przyjmuje cokolwiek. */
37
+ children?: ReactNode;
38
+ }
39
+ /**
40
+ * Pojedyncza wiadomość czatu — bąbel z treścią, godziną, statusem wysyłki i opcjonalnym
41
+ * awatarem, cytatem, załącznikami i reakcjami.
42
+ *
43
+ * Działa na dwa sposoby: dostaje gotowy obiekt przez `message` (tak używa go `KptChat`)
44
+ * albo pojedyncze propsy, gdy budujesz listę samodzielnie.
45
+ *
46
+ * Wygląd wewnątrz grupy steruje `position`: zapadnięty narożnik od strony nadawcy i ogonek
47
+ * tylko przy ostatnim bąblu. Wylicza to `buildChatRows()` z `@konce-pt/chat`.
48
+ *
49
+ * @example
50
+ * <KptChatMessage message={msg} align="end" position="last" />
51
+ * <KptChatMessage align="end" status="failed" onRetry={resend}>Nie poszło</KptChatMessage>
52
+ */
53
+ export declare function KptChatMessage({ message, author, text, time, attachments, reactions, replyTo, status, align, variant, position, system, showAuthor, showAvatar, showMeta, tail, actions, reactionOptions, onRetry, onReply, onReact, onQuoted, children, locale, dictionary, className, ...rest }: Readonly<KptChatMessageProps>): import("react").JSX.Element;
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { KPT_CHAT_REACTIONS, canRetry, toDate } from '@konce-pt/chat';
4
+ import { cn } from "../internal/cn.js";
5
+ import { useKptMessages } from "../i18n/i18n-context.js";
6
+ import { KptAvatar } from "../avatar/avatar.js";
7
+ import { KptIcon } from "../icon/icon.js";
8
+ import { KptIconButton } from "../icon-button/icon-button.js";
9
+ /** Ikona statusu wysyłki. `read` używa tej samej ikony co `delivered`, różni je kolor. */
10
+ const STATUS_ICONS = {
11
+ sending: 'clock',
12
+ sent: 'check',
13
+ delivered: 'checks',
14
+ read: 'checks',
15
+ failed: 'alert-triangle',
16
+ };
17
+ /**
18
+ * Pojedyncza wiadomość czatu — bąbel z treścią, godziną, statusem wysyłki i opcjonalnym
19
+ * awatarem, cytatem, załącznikami i reakcjami.
20
+ *
21
+ * Działa na dwa sposoby: dostaje gotowy obiekt przez `message` (tak używa go `KptChat`)
22
+ * albo pojedyncze propsy, gdy budujesz listę samodzielnie.
23
+ *
24
+ * Wygląd wewnątrz grupy steruje `position`: zapadnięty narożnik od strony nadawcy i ogonek
25
+ * tylko przy ostatnim bąblu. Wylicza to `buildChatRows()` z `@konce-pt/chat`.
26
+ *
27
+ * @example
28
+ * <KptChatMessage message={msg} align="end" position="last" />
29
+ * <KptChatMessage align="end" status="failed" onRetry={resend}>Nie poszło</KptChatMessage>
30
+ */
31
+ export function KptChatMessage({ message = null, author = null, text = null, time = null, attachments = null, reactions = null, replyTo = null, status = null, align = 'start', variant = null, position = 'single', system = false, showAuthor = false, showAvatar = false, showMeta = true, tail = true, actions = false, reactionOptions = KPT_CHAT_REACTIONS, onRetry, onReply, onReact, onQuoted, children, locale, dictionary, className, ...rest }) {
32
+ const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
33
+ const rowRef = useRef(null);
34
+ /** Czy wybierak reakcji jest otwarty. */
35
+ const [pickerOpen, setPickerOpen] = useState(false);
36
+ // Kliknięcie obok zamyka wybierak. Sam `onBlur` nie wystarcza: w Safari kliknięcie w przycisk
37
+ // nie przenosi na niego fokusu, więc wybierak zostawałby otwarty na zawsze. Faza przechwytywania
38
+ // i `pointerdown` (nie `click`) — własne `onClick` opcji zdąży się wykonać przed zamknięciem.
39
+ useEffect(() => {
40
+ if (!pickerOpen)
41
+ return;
42
+ const close = (event) => {
43
+ if (!rowRef.current?.contains(event.target))
44
+ setPickerOpen(false);
45
+ };
46
+ document.addEventListener('pointerdown', close, true);
47
+ return () => document.removeEventListener('pointerdown', close, true);
48
+ }, [pickerOpen]);
49
+ const isSystem = system || (message?.system ?? false);
50
+ const resolvedAuthor = author ?? message?.author ?? null;
51
+ const resolvedText = text ?? message?.text ?? '';
52
+ const resolvedStatus = status ?? message?.status ?? null;
53
+ const resolvedReplyTo = replyTo ?? message?.replyTo ?? null;
54
+ const resolvedAttachments = attachments ?? message?.attachments ?? [];
55
+ const resolvedReactions = reactions ?? message?.reactions ?? [];
56
+ const rawTime = time ?? message?.sentAt ?? null;
57
+ const sentAt = rawTime === null ? null : toDate(rawTime);
58
+ const timeLabel = sentAt
59
+ ? new Intl.DateTimeFormat(activeLocale, { hour: '2-digit', minute: '2-digit' }).format(sentAt)
60
+ : '';
61
+ /*
62
+ * Wariant domyślny bierze się z wyrównania. Własna wiadomość dostaje pełny `solid`,
63
+ * bo motyw biblioteki jest monochromatyczny: pastelowy `primary-subtle` ma tę samą
64
+ * jasność co `muted`, więc własny i cudzy bąbel byłyby nie do odróżnienia. Kto woli
65
+ * ciszej, ustawia `variant="primary"` ręcznie.
66
+ */
67
+ const resolvedVariant = isSystem
68
+ ? 'system'
69
+ : (variant ?? (align === 'end' ? 'solid' : 'neutral'));
70
+ /* Ogonek tylko na końcu grupy i tylko tam, gdzie bąbel ma tło, z którego da się go wyciąć. */
71
+ const hasTail = tail &&
72
+ resolvedVariant !== 'ghost' &&
73
+ resolvedVariant !== 'system' &&
74
+ (position === 'last' || position === 'single');
75
+ const statusIcon = resolvedStatus ? STATUS_ICONS[resolvedStatus] : null;
76
+ const retryable = canRetry(resolvedStatus);
77
+ /*
78
+ * Etykieta dla czytnika ekranu. Bez niej z bąbla płynie sama treść — bez nadawcy,
79
+ * godziny i informacji, że wiadomość się nie wysłała.
80
+ */
81
+ const ariaLabel = isSystem
82
+ ? resolvedText
83
+ : [resolvedAuthor?.name, timeLabel, resolvedStatus ? t('chat.' + resolvedStatus) : '']
84
+ .filter(Boolean)
85
+ .join(', ');
86
+ if (isSystem) {
87
+ return (_jsx("div", { className: cn('kpt-chat-message-host', className), role: "note", "aria-label": ariaLabel, ...rest, children: _jsx("p", { className: "kpt-chat-message__system", children: resolvedText }) }));
88
+ }
89
+ return (_jsx("div", { className: cn('kpt-chat-message-host', className), role: "article", "aria-label": ariaLabel, ...rest, children: _jsxs("div", { ref: rowRef, className: "kpt-chat-message__row", "data-align": align, children: [align === 'start' ? (_jsx("span", { className: "kpt-chat-message__avatar", "aria-hidden": "true", children: showAvatar ? (_jsx(KptAvatar, { size: "sm", src: resolvedAuthor?.avatar ?? null, name: resolvedAuthor?.name ?? '' })) : null })) : null, _jsxs("div", { className: "kpt-chat-message__stack", children: [showAuthor && resolvedAuthor ? (_jsx("span", { className: "kpt-chat-message__author", children: resolvedAuthor.name })) : null, _jsxs("div", { className: "kpt-chat-message__bubble", "data-variant": resolvedVariant, "data-align": align, "data-position": position, "data-status": resolvedStatus ?? undefined, "data-tail": hasTail ? '' : undefined, children: [resolvedReplyTo ? (_jsxs("button", { type: "button", className: "kpt-chat-message__quote", onClick: () => onQuoted?.(resolvedReplyTo), children: [_jsx("span", { className: "kpt-chat-message__quote-author", children: resolvedReplyTo.author }), _jsx("span", { className: "kpt-chat-message__quote-text", children: resolvedReplyTo.excerpt })] })) : null, resolvedText ? _jsx("p", { className: "kpt-chat-message__text", children: resolvedText }) : null, children, resolvedAttachments.length ? (_jsx("ul", { className: "kpt-chat-message__files", children: resolvedAttachments.map((file) => (_jsxs("li", { className: "kpt-chat-message__file", children: [file.thumbnail ? (_jsx("img", { className: "kpt-chat-message__file-thumb", src: file.thumbnail, alt: file.name })) : (_jsx(KptIcon, { name: "paperclip" })), _jsx("a", { className: "kpt-chat-message__file-name", href: file.url, download: file.url ? file.name : undefined, children: file.name }), file.size ? (_jsx("span", { className: "kpt-chat-message__file-size", children: fileSize(file.size) })) : null] }, file.id))) })) : null, showMeta ? (_jsxs("span", { className: "kpt-chat-message__meta", children: [timeLabel ? (_jsx("time", { className: "kpt-chat-message__time", dateTime: sentAt?.toISOString(), children: timeLabel })) : null, statusIcon ? (_jsx(KptIcon, { className: "kpt-chat-message__status", name: statusIcon })) : null] })) : null, resolvedReactions.length ? (_jsx("ul", { className: "kpt-chat-message__reactions", "data-align": align, children: resolvedReactions.map((reaction) => (_jsxs("li", { className: "kpt-chat-message__reaction", "data-reacted": reaction.reacted ? '' : undefined, children: [_jsx("span", { "aria-hidden": "true", children: reaction.emoji }), _jsx("span", { className: "kpt-chat-message__reaction-count", children: reaction.count })] }, reaction.emoji))) })) : null] })] }), _jsx("span", { className: "kpt-chat-message__actions", "data-open": pickerOpen ? '' : undefined, children: retryable ? (_jsx(KptIconButton, { hostProps: { className: 'kpt-chat-message__retry' }, size: "sm", "aria-label": t('chat.retry'), onClick: () => onRetry?.(), children: _jsx(KptIcon, { name: "refresh" }) })) : actions ? (_jsxs(_Fragment, { children: [_jsx(KptIconButton, { size: "sm", "aria-label": t('chat.replyTo', { author: resolvedAuthor?.name ?? '' }), onClick: () => onReply?.(), children: _jsx(KptIcon, { name: "corner-up-left" }) }), _jsxs("span", { className: "kpt-chat-message__react", onKeyDown: (event) => {
90
+ if (event.key === 'Escape')
91
+ setPickerOpen(false);
92
+ }, onBlur: (event) => {
93
+ // Przechodzenie Tabem między opcjami wybieraka go nie zamyka.
94
+ if (!event.currentTarget.contains(event.relatedTarget))
95
+ setPickerOpen(false);
96
+ }, children: [_jsx(KptIconButton, { size: "sm", "aria-label": t('chat.react'), "aria-expanded": pickerOpen, onClick: () => setPickerOpen((open) => !open), children: _jsx(KptIcon, { name: "mood-smile" }) }), pickerOpen ? (_jsx("span", { className: "kpt-chat-message__picker", role: "group", "aria-label": t('chat.react'), children: reactionOptions.map((option) => {
97
+ const label = t('chat.react' + option.kind[0].toUpperCase() + option.kind.slice(1));
98
+ const reacted = resolvedReactions.some((item) => item.emoji === option.emoji && item.reacted);
99
+ return (_jsx("button", { type: "button", className: "kpt-chat-message__pick", "aria-label": label, title: label, "aria-pressed": reacted, onClick: () => {
100
+ onReact?.(option.emoji);
101
+ setPickerOpen(false);
102
+ }, children: _jsx("span", { "aria-hidden": "true", children: option.emoji }) }, option.kind));
103
+ }) })) : null] })] })) : null })] }) }));
104
+ }
105
+ /** Rozmiar załącznika w kB/MB — surowe bajty nic nie mówią przy pliku obok nazwy. */
106
+ function fileSize(bytes) {
107
+ if (bytes < 1024)
108
+ return bytes + ' B';
109
+ if (bytes < 1024 * 1024)
110
+ return Math.round(bytes / 1024) + ' kB';
111
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
112
+ }
@@ -0,0 +1,73 @@
1
+ # KptChatMessage (@konce-pt/react/chat)
2
+
3
+ A single chat bubble: content, a clock, a send status, and optionally an avatar, a quote,
4
+ attachments and reactions.
5
+ Import: `import { KptChatMessage } from '@konce-pt/react/chat';`
6
+
7
+ ## DOM
8
+ <div class="kpt-chat-message-host" role="article" aria-label="Anna, 09:22, Read">
9
+ <div class="kpt-chat-message__row" data-align="start">
10
+ <span class="kpt-chat-message__avatar">…KptAvatar…</span>
11
+ <div class="kpt-chat-message__stack">
12
+ <span class="kpt-chat-message__author">Anna Kowalska</span>
13
+ <div class="kpt-chat-message__bubble"
14
+ data-variant="neutral" data-align="start" data-position="last" data-tail>
15
+ <button class="kpt-chat-message__quote">…</button>
16
+ <p class="kpt-chat-message__text">…</p>
17
+ <ul class="kpt-chat-message__files">…</ul>
18
+ <span class="kpt-chat-message__meta"><time>09:22</time>…</span>
19
+ <ul class="kpt-chat-message__reactions">…</ul>
20
+ </div>
21
+ </div>
22
+ <span class="kpt-chat-message__actions">…KptIconButton, .kpt-chat-message__picker…</span>
23
+ </div>
24
+ </div>
25
+
26
+ ## Props
27
+ - `message`: `KptChatMessage` — the whole object; the single props below win over it
28
+ - `author`, `text`, `time`, `status`, `attachments`, `reactions`, `replyTo`
29
+ - `align`: `'start' | 'end'`; `variant`: `'neutral' | 'primary' | 'solid' | 'ghost' | 'system'`
30
+ - `position`: `'single' | 'first' | 'middle' | 'last'`; `system`: boolean
31
+ - `showAuthor`, `showAvatar`, `showMeta` (default true), `tail` (default true), `actions`
32
+ - `reactionOptions`: `readonly KptChatReactionOption[]` (default `KPT_CHAT_REACTIONS`) — the picker set
33
+ - `onRetry`, `onReply`: `() => void`; `onReact`: `(emoji: string) => void`; `onQuoted`: `(quote) => void`
34
+ - `children` — bubble content; it renders next to `text`, so the bubble takes anything
35
+ - `className`, `ref` and every other `<div>` prop go to the host
36
+
37
+ ## The default variant follows the alignment
38
+ Your own message gets the full `solid`, someone else's gets `neutral`. The theme is monochrome, so
39
+ the difference between bubbles is lightness, not hue — `--kpt-color-primary-subtle` has exactly the
40
+ lightness of `--kpt-color-muted`, which would make the two sides indistinguishable. For a quieter
41
+ list set `variant="primary"` by hand.
42
+
43
+ ## Grouping is what the shape is made of
44
+ `position` decides which corner collapses and which bubble wears the tail — inside a group the
45
+ corner on the sender's side drops to `--kpt-radius-sm`. `KptChat` gets all of it from
46
+ `buildChatRows()`; set it by hand only when you build the list yourself. The tail is skipped for
47
+ `ghost` and `system`, which have no background to cut it out of.
48
+
49
+ ## An unsent message must be able to say so
50
+ `status="failed"` outlines the bubble and puts a retry arrow **outside** it, in the action column
51
+ on the outer side of the list. `KptChat` also keeps the meta row visible on such a bubble even in
52
+ the middle of a group — the arrow with nothing to explain it would be a mystery.
53
+ Statuses: `sending` (clock), `sent` (check), `delivered` (double check), `read` (the same icon in
54
+ the accent colour) and `failed`.
55
+
56
+ ## Examples
57
+ <KptChatMessage message={msg} align="end" position="last" />
58
+ <KptChatMessage align="end" status="failed" onRetry={resend}>It did not go out</KptChatMessage>
59
+ <KptChatMessage system text="Anna joined the conversation" />
60
+
61
+ ## i18n
62
+ `retry`, `react`, `replyTo` (`{author}`) and the status names from the `chat` namespace. The clock
63
+ is formatted by `Intl` in the active locale.
64
+
65
+ ## Tokens
66
+ `--kpt-color-muted` (incoming) / `--kpt-color-primary` + `--kpt-color-on-primary` (own),
67
+ failed `--kpt-color-danger-subtle` + `--kpt-color-danger-border` — the failed rule also resets the
68
+ text colour, or a white `solid` label would land on pale pink.
69
+
70
+ ## Accessibility
71
+ The host is an `article` labelled with author, time and status — content alone would say nothing
72
+ about who wrote it or whether it was sent. The status icon is `aria-hidden`; the meaning travels in
73
+ the label. A system notice is a `note`, never an `article`.
@@ -0,0 +1,23 @@
1
+ import type { ComponentPropsWithRef } from 'react';
2
+ import type { KptChatAuthor } from '@konce-pt/chat';
3
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
4
+ export interface KptChatTypingProps extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
5
+ /** Osoby piszące w tej chwili. Pusta lista chowa wskaźnik. */
6
+ authors?: readonly KptChatAuthor[];
7
+ /** Własna etykieta zamiast tej ze słownika. */
8
+ label?: string;
9
+ }
10
+ /**
11
+ * Wskaźnik pisania — trzy kropki i informacja, kto pisze.
12
+ *
13
+ * Pusta lista autorów nie renderuje niczego, więc można go trzymać w drzewie na stałe
14
+ * i sterować wyłącznie danymi.
15
+ *
16
+ * Element jest `role="status"` z `aria-live="polite"`: czytnik ogłasza wejście i zniknięcie
17
+ * wskaźnika, ale nie przerywa czytania wiadomości. Sama animacja kropek nic nie mówi —
18
+ * informację niesie etykieta.
19
+ *
20
+ * @example
21
+ * <KptChatTyping authors={typing} />
22
+ */
23
+ export declare function KptChatTyping({ authors, label, locale, dictionary, className, ...rest }: Readonly<KptChatTypingProps>): import("react").JSX.Element | null;