@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
@@ -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) inside a
4
- `.kpt-button-host` wrapper — two DOM levels, exactly like the Angular port, because the host
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 `<button>`
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 variant="outline" size="sm">Cancel</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
- <form action={submitAction}><KptButton type="submit">Send</KptButton></form>
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-tonal-{bg,fg,bg-hover}`,
44
- `--kpt-button-outline-{border,fg,bg-hover}`, `--kpt-button-text-{fg,bg-hover}`.
45
- The 'danger' variant uses `--kpt-color-danger` / `--kpt-color-on-danger`.
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 (_jsx("button", { type: "button", className: cn('kpt-button-group__item', active && 'is-active'), disabled: disabled || option.disabled, "aria-pressed": active, onClick: () => select(option), children: option.label }, String(option.value)));
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">List</button>
10
- <button type="button" class="kpt-button-group__item" aria-pressed="false">Grid</button>
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;