@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
@@ -7,16 +7,19 @@ import { KptIcon } from "../icon/icon.js";
7
7
  const EMPTY = [];
8
8
  /**
9
9
  * PickList — transfer elementów między dwiema listami. Klik zaznacza, przyciski pośrodku
10
- * przenoszą zaznaczone albo wszystko.
10
+ * przenoszą zaznaczone albo wszystko, dwuklik przerzuca pojedynczy element na drugą stronę.
11
+ * Z `dragdrop` dochodzi przeciąganie — porządkowanie wewnątrz panelu i przenoszenie na
12
+ * konkretną pozycję drugiego panelu.
11
13
  *
12
14
  * @example
13
15
  * <KptPickList
14
16
  * source={available} onSourceChange={setAvailable}
15
17
  * target={selected} onTargetChange={setSelected}
18
+ * dragdrop
16
19
  * renderItem={(item) => item.name}
17
20
  * />
18
21
  */
19
- export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, target, defaultTarget = EMPTY, onTargetChange, sourceHeader, targetHeader, renderItem, locale, dictionary, className, ...rest }) {
22
+ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, target, defaultTarget = EMPTY, onTargetChange, sourceHeader, targetHeader, dragdrop = false, renderItem, locale, dictionary, className, ...rest }) {
20
23
  const { t } = useKptMessages({ locale, dictionary });
21
24
  const [left, setLeft] = useControllableState({
22
25
  value: source,
@@ -32,34 +35,137 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
32
35
  // po obu stronach się przesuwają, a referencja zostaje ta sama.
33
36
  const [selLeft, setSelLeft] = useState(new Set());
34
37
  const [selRight, setSelRight] = useState(new Set());
38
+ /** Przeciągany element — strona i pozycja w jej tablicy. */
39
+ const [drag, setDrag] = useState(null);
40
+ /** Element pod kursorem — rysuje wskaźnik miejsca upuszczenia. */
41
+ const [over, setOver] = useState(null);
42
+ /** Panel, nad którego pustym obszarem jest kursor — upuszczenie tam dopina na koniec. */
43
+ const [listOver, setListOver] = useState(null);
44
+ const listOf = (side) => (side === 'source' ? left : right);
45
+ const setListOf = (side) => (side === 'source' ? setLeft : setRight);
46
+ const selectionOf = (side) => (side === 'source' ? selLeft : selRight);
47
+ const setSelectionOf = (side) => side === 'source' ? setSelLeft : setSelRight;
48
+ const other = (side) => (side === 'source' ? 'target' : 'source');
35
49
  const toggle = (side, item) => {
36
- const [selection, setSelection] = side === 'source' ? [selLeft, setSelLeft] : [selRight, setSelRight];
37
- const next = new Set(selection);
50
+ const next = new Set(selectionOf(side));
38
51
  if (next.has(item))
39
52
  next.delete(item);
40
53
  else
41
54
  next.add(item);
42
- setSelection(next);
55
+ setSelectionOf(side)(next);
43
56
  };
44
57
  const move = (from, all) => {
45
- const fromItems = from === 'source' ? left : right;
46
- const toItems = from === 'source' ? right : left;
47
- const setFrom = from === 'source' ? setLeft : setRight;
48
- const setTo = from === 'source' ? setRight : setLeft;
49
- const selection = from === 'source' ? selLeft : selRight;
50
- const setSelection = from === 'source' ? setSelLeft : setSelRight;
58
+ const to = other(from);
59
+ const fromItems = listOf(from);
60
+ const selection = selectionOf(from);
51
61
  const moving = all ? fromItems : fromItems.filter((item) => selection.has(item));
52
62
  if (!moving.length)
53
63
  return;
54
- setFrom(fromItems.filter((item) => !moving.includes(item)));
55
- setTo([...toItems, ...moving]);
56
- setSelection(new Set());
64
+ setListOf(from)(fromItems.filter((item) => !moving.includes(item)));
65
+ setListOf(to)([...listOf(to), ...moving]);
66
+ setSelectionOf(from)(new Set());
67
+ };
68
+ /** Przenosi jeden element między panelami, wstawiając go pod wskazanym indeksem. */
69
+ const transfer = (fromSide, fromIndex, toSide, toIndex) => {
70
+ const fromItems = [...listOf(fromSide)];
71
+ const [moved] = fromItems.splice(fromIndex, 1);
72
+ if (moved === undefined)
73
+ return;
74
+ const toItems = [...listOf(toSide)];
75
+ toItems.splice(toIndex, 0, moved);
76
+ setListOf(fromSide)(fromItems);
77
+ setListOf(toSide)(toItems);
78
+ // Element opuszcza panel — jego zaznaczenie po tamtej stronie przestaje cokolwiek znaczyć.
79
+ const selection = selectionOf(fromSide);
80
+ if (selection.has(moved)) {
81
+ const next = new Set(selection);
82
+ next.delete(moved);
83
+ setSelectionOf(fromSide)(next);
84
+ }
85
+ };
86
+ /** Dwuklik: przenosi wyłącznie klikniętą pozycję, niezależnie od zaznaczenia. */
87
+ const moveOne = (from, item) => {
88
+ const index = listOf(from).indexOf(item);
89
+ if (index === -1)
90
+ return;
91
+ const to = other(from);
92
+ transfer(from, index, to, listOf(to).length);
93
+ };
94
+ const clearDrag = () => {
95
+ setDrag(null);
96
+ setOver(null);
97
+ setListOver(null);
98
+ };
99
+ const onDragStart = (side, index, event) => {
100
+ if (!dragdrop)
101
+ return;
102
+ setDrag({ side, index });
103
+ event.dataTransfer.setData('text/plain', String(index));
104
+ event.dataTransfer.effectAllowed = 'move';
105
+ };
106
+ const onDragOver = (side, index, event) => {
107
+ if (drag === null)
108
+ return;
109
+ // Zatrzymujemy propagację, żeby `<ul>` nie uznał, że kursor jest nad pustym obszarem.
110
+ event.preventDefault();
111
+ event.stopPropagation();
112
+ setOver({ side, index });
113
+ setListOver(null);
114
+ };
115
+ const onDrop = (side, index, event) => {
116
+ event.preventDefault();
117
+ event.stopPropagation();
118
+ const from = drag;
119
+ clearDrag();
120
+ if (from === null)
121
+ return;
122
+ if (from.side !== side) {
123
+ transfer(from.side, from.index, side, index);
124
+ return;
125
+ }
126
+ if (from.index === index)
127
+ return;
128
+ // Indeks celu liczony na tablicy sprzed usunięcia — element ląduje na pozycji tego,
129
+ // na który go upuszczono (ten sam wzór co przestawianie kolumn w KptDataTable).
130
+ const next = [...listOf(side)];
131
+ const [moved] = next.splice(from.index, 1);
132
+ if (moved === undefined)
133
+ return;
134
+ next.splice(index, 0, moved);
135
+ setListOf(side)(next);
136
+ };
137
+ const onListDragOver = (side, event) => {
138
+ if (drag === null)
139
+ return;
140
+ event.preventDefault();
141
+ setListOver(side);
142
+ setOver(null);
143
+ };
144
+ const onListDrop = (side, event) => {
145
+ event.preventDefault();
146
+ const from = drag;
147
+ clearDrag();
148
+ if (from === null)
149
+ return;
150
+ if (from.side !== side) {
151
+ transfer(from.side, from.index, side, listOf(side).length);
152
+ return;
153
+ }
154
+ const next = [...listOf(side)];
155
+ const [moved] = next.splice(from.index, 1);
156
+ if (moved === undefined)
157
+ return;
158
+ next.push(moved);
159
+ setListOf(side)(next);
57
160
  };
58
161
  const column = (side) => {
59
- const items = side === 'source' ? left : right;
60
- const selection = side === 'source' ? selLeft : selRight;
162
+ const items = listOf(side);
163
+ const selection = selectionOf(side);
61
164
  const header = side === 'source' ? (sourceHeader ?? t('pickList.available')) : (targetHeader ?? t('pickList.selected'));
62
- return (_jsxs("div", { className: "kpt-pick-list__col", children: [_jsx("div", { className: "kpt-pick-list__header", children: header }), _jsx("ul", { className: "kpt-pick-list__items", children: items.map((item, index) => (_jsx("li", { className: cn('kpt-pick-list__item', selection.has(item) && 'is-selected'), onClick: () => toggle(side, item), children: renderItem(item, index) }, index))) })] }));
165
+ return (_jsxs("div", { className: "kpt-pick-list__col", children: [_jsx("div", { className: "kpt-pick-list__header", children: header }), _jsx("ul", { className: cn('kpt-pick-list__items', listOver === side && 'is-dragover'), onDragOver: (event) => onListDragOver(side, event), onDragLeave: () => setListOver(null), onDrop: (event) => onListDrop(side, event), children: items.map((item, index) => {
166
+ const dragging = drag?.side === side && drag.index === index;
167
+ return (_jsx("li", { className: cn('kpt-pick-list__item', selection.has(item) && 'is-selected', dragging && 'is-dragging', dragdrop && 'kpt-pick-list__item--draggable', over?.side === side && over.index === index && !dragging && 'kpt-pick-list__item--dragover'), draggable: dragdrop || undefined, onClick: () => toggle(side, item), onDoubleClick: () => moveOne(side, item), onDragStart: (event) => onDragStart(side, index, event), onDragOver: (event) => onDragOver(side, index, event), onDragLeave: () => setOver(null), onDrop: (event) => onDrop(side, index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index));
168
+ }) })] }));
63
169
  };
64
170
  return (_jsx("div", { className: cn('kpt-pick-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-pick-list", children: [column('source'), _jsxs("div", { className: "kpt-pick-list__controls", children: [_jsxs("button", { type: "button", "aria-label": t('pickList.moveAllRight'), onClick: () => move('source', true), children: [_jsx(KptIcon, { name: "chevron-right" }), _jsx(KptIcon, { name: "chevron-right" })] }), _jsx("button", { type: "button", "aria-label": t('pickList.moveRight'), onClick: () => move('source', false), children: _jsx(KptIcon, { name: "chevron-right" }) }), _jsx("button", { type: "button", "aria-label": t('pickList.moveLeft'), onClick: () => move('target', false), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsxs("button", { type: "button", "aria-label": t('pickList.moveAllLeft'), onClick: () => move('target', true), children: [_jsx(KptIcon, { name: "chevron-left" }), _jsx(KptIcon, { name: "chevron-left" })] })] }), column('target')] }) }));
65
171
  }
@@ -36,8 +36,12 @@ export { KptAvatar } from './avatar/avatar.tsx';
36
36
  export type { KptAvatarProps, KptAvatarShape, KptAvatarSize } from './avatar/avatar.tsx';
37
37
  export { KptBadge } from './badge/badge.tsx';
38
38
  export type { KptBadgeProps, KptBadgeSize, KptBadgeVariant } from './badge/badge.tsx';
39
+ export { KptBackdrop } from './backdrop/backdrop.tsx';
40
+ export type { KptBackdropAnchor, KptBackdropEffect, KptBackdropProps } from './backdrop/backdrop.tsx';
41
+ export type { KptBackdropRenderOptions, KptBackdropRenderer, KptOklab, KptRgb, KptSignalGridCell, KptSignalGridFrame, KptSignalGridOptions, KptWaveOptions, KptWaveSettings, } from '@konce-pt/backdrop';
42
+ export { KPT_SIGNAL_GRID_CELL, KPT_SIGNAL_GRID_DENSITY, KPT_SIGNAL_GRID_FALLOFF, KPT_SIGNAL_GRID_GAP, KPT_WAVE_AMPLITUDE, KPT_WAVE_BAND, KPT_WAVE_RESOLUTION, KPT_WAVE_ROTATION, buildSignalGrid, createBackdropRenderer, mixOklch, readCssColor, resolveWave, shiftHue, signalGridAlpha, signalGridDensity, waveIntensity, waveRamp, } from '@konce-pt/backdrop';
39
43
  export { KptChip } from './chip/chip.tsx';
40
- export type { KptChipProps, KptChipVariant } from './chip/chip.tsx';
44
+ export type { KptChipProps, KptChipTextTransform, KptChipVariant } from './chip/chip.tsx';
41
45
  export { KptDialog, KptDialogFooter } from './dialog/dialog.tsx';
42
46
  export type { KptDialogProps } from './dialog/dialog.tsx';
43
47
  export { KptDrawer } from './drawer/drawer.tsx';
@@ -102,6 +106,14 @@ export { KptTooltip } from './tooltip/tooltip.tsx';
102
106
  export type { KptTooltipPosition, KptTooltipProps } from './tooltip/tooltip.tsx';
103
107
  export { KptPopover } from './popover/popover.tsx';
104
108
  export type { KptPopoverProps } from './popover/popover.tsx';
109
+ export { KptPeek } from './peek/peek.tsx';
110
+ export type { KptPeekPlacement, KptPeekProps } from './peek/peek.tsx';
111
+ export { KptMention } from './mention/mention.tsx';
112
+ export type { KptMentionProps } from './mention/mention.tsx';
113
+ export { KptMentions } from './mention/mentions.tsx';
114
+ export type { KptMentionsProps } from './mention/mentions.tsx';
115
+ export type { KptMentionItem, KptMentionQuery, KptMentionSegment } from '@konce-pt/mention';
116
+ export { KPT_MENTION_LIMIT, applyMention, collectMentions, filterMentions, foldMentionText, mentionQuery, mentionToken, parseMentionText, } from '@konce-pt/mention';
105
117
  export { KptOverlay } from './internal/overlay.tsx';
106
118
  export type { KptOverlayProps } from './internal/overlay.tsx';
107
119
  export { KptButtonGroup } from './button-group/button-group.tsx';
@@ -27,6 +27,8 @@ export { KptIcon } from "./icon/icon.js";
27
27
  export { getKptIcon, hasKptIcon, registerKptIcons } from "./icon/icon-registry.js";
28
28
  export { KptAvatar } from "./avatar/avatar.js";
29
29
  export { KptBadge } from "./badge/badge.js";
30
+ export { KptBackdrop } from "./backdrop/backdrop.js";
31
+ export { KPT_SIGNAL_GRID_CELL, KPT_SIGNAL_GRID_DENSITY, KPT_SIGNAL_GRID_FALLOFF, KPT_SIGNAL_GRID_GAP, KPT_WAVE_AMPLITUDE, KPT_WAVE_BAND, KPT_WAVE_RESOLUTION, KPT_WAVE_ROTATION, buildSignalGrid, createBackdropRenderer, mixOklch, readCssColor, resolveWave, shiftHue, signalGridAlpha, signalGridDensity, waveIntensity, waveRamp, } from '@konce-pt/backdrop';
30
32
  export { KptChip } from "./chip/chip.js";
31
33
  // Komponenty — nakładki
32
34
  export { KptDialog, KptDialogFooter } from "./dialog/dialog.js";
@@ -60,6 +62,10 @@ export { KptBreadcrumb } from "./breadcrumb/breadcrumb.js";
60
62
  export { KptContextMenu } from "./context-menu/context-menu.js";
61
63
  export { KptTooltip } from "./tooltip/tooltip.js";
62
64
  export { KptPopover } from "./popover/popover.js";
65
+ export { KptPeek } from "./peek/peek.js";
66
+ export { KptMention } from "./mention/mention.js";
67
+ export { KptMentions } from "./mention/mentions.js";
68
+ export { KPT_MENTION_LIMIT, applyMention, collectMentions, filterMentions, foldMentionText, mentionQuery, mentionToken, parseMentionText, } from '@konce-pt/mention';
63
69
  export { KptOverlay } from "./internal/overlay.js";
64
70
  // Komponenty — formularze
65
71
  export { KptButtonGroup } from "./button-group/button-group.js";
@@ -9,8 +9,18 @@ Import: `import { KptRichText, type KptRichTool } from '@konce-pt/react';`
9
9
  - `toolbar`: `KptRichTool[] | KptRichTool[][]` — a flat list, or groups = sections separated by a
10
10
  divider. Leaving a tool or a group out disables it; the full toolbar in 5 sections by default.
11
11
  - `placeholder`, `maxListLevel` (6), `disabled`, `invalid`, `touched`, `onTouch()`
12
+ - `mentions`: `readonly KptMentionItem[]`, `mentionLimit` (8), `onMention(item)` — suggestions
13
+ for `@`; an empty list turns the mechanism off, so `@` stays an ordinary character
12
14
  - `className`, `ref` and every other `<div>` prop go to the host
13
15
 
16
+ ## Mentions (`@`)
17
+ With `mentions` set, typing `@` opens a suggestion list anchored at the caret: arrows move through
18
+ it, Enter or Tab inserts, Escape closes. The query is read from the **text node** at the caret and
19
+ recognised by `mentionQuery()` from `@konce-pt/mention` — the same rules as in the Angular port.
20
+ An inserted mention is `<span class="kpt-mention" data-mention="id" contenteditable="false">@Name`
21
+ plus a non-breaking space: the id survives saving, the flag keeps the mention in one piece and the
22
+ space gives the caret a place to stand after it.
23
+
14
24
  ## Tools (KptRichTool)
15
25
  Clipboard: `copy`, `paste` (sanitized), `pasteText`. Style: `heading` (a panel: Normal + H1–H6).
16
26
  Format: `bold`, `italic`, `underline`, `strike`, `ul`, `ol`, `indent`, `outdent`.
@@ -1,4 +1,5 @@
1
1
  import { type ComponentPropsWithRef } from 'react';
2
+ import { type KptMentionItem } from '@konce-pt/mention';
2
3
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
4
  export type KptRichTool = 'copy' | 'paste' | 'pasteText' | 'heading' | 'bold' | 'italic' | 'underline' | 'strike' | 'ul' | 'ol' | 'indent' | 'outdent' | 'foreColor' | 'backColor' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'alignJustify' | 'link' | 'table' | 'clear' | 'html';
4
5
  /** Pasek: płaska lista narzędzi albo grupy — sekcje rozdzielone separatorem. */
@@ -16,6 +17,15 @@ export interface KptRichTextProps extends Omit<ComponentPropsWithRef<'div'>, 'on
16
17
  invalid?: boolean;
17
18
  touched?: boolean;
18
19
  onTouch?: () => void;
20
+ /**
21
+ * Pozycje do podpowiedzi wzmianek. Pusta lista wyłącza mechanizm: `@` zostaje wtedy zwykłym
22
+ * znakiem, bo nie ma czego podpowiadać.
23
+ */
24
+ mentions?: readonly KptMentionItem[];
25
+ /** Ile podpowiedzi naraz. */
26
+ mentionLimit?: number;
27
+ /** Wstawiona wzmianka — moment na powiadomienie osoby albo dopięcie powiązania. */
28
+ onMention?: (item: KptMentionItem) => void;
19
29
  }
20
30
  /**
21
31
  * Lekki edytor tekstu sformatowanego na `contenteditable`, z konfigurowalnym paskiem sekcji
@@ -25,4 +35,4 @@ export interface KptRichTextProps extends Omit<ComponentPropsWithRef<'div'>, 'on
25
35
  * <KptRichText value={html} onValueChange={setHtml} />
26
36
  * <KptRichText value={html} onValueChange={setHtml} toolbar={[['bold', 'italic'], ['link']]} />
27
37
  */
28
- export declare function KptRichText({ value, defaultValue, onValueChange, toolbar, placeholder, maxListLevel, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptRichTextProps>): import("react").JSX.Element;
38
+ export declare function KptRichText({ value, defaultValue, onValueChange, toolbar, placeholder, maxListLevel, disabled, invalid, touched, onTouch, mentions, mentionLimit, onMention, locale, dictionary, className, ...rest }: Readonly<KptRichTextProps>): import("react").JSX.Element;
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState, } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { cn } from "../internal/cn.js";
5
5
  import { useControllableState } from "../internal/use-controllable-state.js";
6
+ import { KPT_MENTION_LIMIT, filterMentions, mentionQuery } from '@konce-pt/mention';
6
7
  import { useKptOverlayContainer } from "../internal/overlay-container.js";
7
8
  import { KptOverlay } from "../internal/overlay.js";
8
9
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -85,7 +86,7 @@ function legacyState(cmd) {
85
86
  * <KptRichText value={html} onValueChange={setHtml} />
86
87
  * <KptRichText value={html} onValueChange={setHtml} toolbar={[['bold', 'italic'], ['link']]} />
87
88
  */
88
- export function KptRichText({ value, defaultValue = '', onValueChange, toolbar = DEFAULT_TOOLBAR, placeholder = '', maxListLevel = 6, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
89
+ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar = DEFAULT_TOOLBAR, placeholder = '', maxListLevel = 6, disabled = false, invalid = false, touched, onTouch, mentions = [], mentionLimit = KPT_MENTION_LIMIT, onMention, locale, dictionary, className, ...rest }) {
89
90
  const { t } = useKptMessages({ locale, dictionary });
90
91
  const container = useKptOverlayContainer();
91
92
  const editorRef = useRef(null);
@@ -105,6 +106,14 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
105
106
  const [sourceView, setSourceView] = useState(false);
106
107
  const [menuOpen, setMenuOpen] = useState(false);
107
108
  const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 });
109
+ // ── Wzmianki (`@`) ──
110
+ const [mentionOpen, setMentionOpen] = useState(false);
111
+ const [mentionMatches, setMentionMatches] = useState([]);
112
+ const [mentionActive, setMentionActive] = useState(0);
113
+ /** Zaczepienie listy w miejscu kursora — punkt, nie element, bo `@` nie ma własnego elementu. */
114
+ const [mentionPoint, setMentionPoint] = useState({ x: 0, y: 0 });
115
+ /** Węzeł tekstowy i zakres zapytania `@…`, który podmieni wybrana pozycja. */
116
+ const mentionRange = useRef(null);
108
117
  const [borderOpen, setBorderOpen] = useState(false);
109
118
  const [borderTarget, setBorderTarget] = useState('table');
110
119
  const [activeCmds, setActiveCmds] = useState(new Set());
@@ -175,13 +184,106 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
175
184
  selCells.current = new Set();
176
185
  setSelCount(0);
177
186
  }, []);
187
+ /** Zamknięcie podpowiedzi — czyści też zapamiętany zakres zapytania. */
188
+ const closeMention = useCallback(() => {
189
+ mentionRange.current = null;
190
+ setMentionMatches([]);
191
+ setMentionOpen(false);
192
+ }, []);
193
+ /** Punkt pod kursorem — lista podpowiedzi zaczepia się o niego, bo `@` nie ma swojego elementu. */
194
+ const caretPoint = useCallback((node, offset) => {
195
+ const range = document.createRange();
196
+ range.setStart(node, Math.min(offset, node.length));
197
+ range.collapse(true);
198
+ const rect = range.getBoundingClientRect();
199
+ // Zwinięty zakres na początku wiersza potrafi dać zerowy prostokąt — wtedy bierzemy
200
+ // krawędź samego edytora, żeby lista nie wylądowała w rogu ekranu.
201
+ if (rect.width === 0 && rect.height === 0) {
202
+ const fallback = editorRef.current?.getBoundingClientRect();
203
+ return { x: fallback?.left ?? 0, y: fallback?.top ?? 0 };
204
+ }
205
+ return { x: rect.left, y: rect.bottom };
206
+ }, []);
207
+ /**
208
+ * Sprawdza, czy kursor stoi w zapytaniu `@…`, i ustawia listę podpowiedzi.
209
+ *
210
+ * Zapytanie liczymy z **węzła tekstowego**, nie z całego HTML-a: w treści siedzą tagi, a `@`
211
+ * ma widzieć tylko to, co użytkownik naprawdę napisał w tym miejscu. Rozpoznanie zostawiamy
212
+ * `mentionQuery()` z `@konce-pt/mention` — te same reguły działają w obu portach.
213
+ */
214
+ const syncMention = useCallback(() => {
215
+ if (disabled || !mentions.length) {
216
+ closeMention();
217
+ return;
218
+ }
219
+ const selection = window.getSelection();
220
+ const node = selection?.anchorNode;
221
+ if (!selection?.isCollapsed || !(node instanceof Text) || !editorRef.current?.contains(node)) {
222
+ closeMention();
223
+ return;
224
+ }
225
+ const query = mentionQuery(node.data, selection.anchorOffset);
226
+ if (!query) {
227
+ closeMention();
228
+ return;
229
+ }
230
+ const matches = filterMentions(mentions, query.query, mentionLimit);
231
+ if (!matches.length) {
232
+ closeMention();
233
+ return;
234
+ }
235
+ mentionRange.current = { node, start: query.start, end: query.end };
236
+ setMentionMatches(matches);
237
+ setMentionActive(0);
238
+ setMentionPoint(caretPoint(node, query.end));
239
+ setMentionOpen(true);
240
+ }, [caretPoint, closeMention, disabled, mentionLimit, mentions]);
241
+ /**
242
+ * Wstawia wzmiankę w miejsce zapytania `@…`.
243
+ *
244
+ * Wzmianka jest elementem, nie samym tekstem: `data-mention` niesie identyfikator, który
245
+ * przeżyje zmianę nazwy, a `contenteditable="false"` trzyma ją w jednym kawałku — bez tego
246
+ * kursor wchodziłby w środek nazwiska i psuł zapis. Spacja po wzmiance daje gdzie postawić
247
+ * kursor: bez niej nie da się wyjść poza element, który nie jest edytowalny.
248
+ */
249
+ const insertMention = useCallback((item) => {
250
+ const target = mentionRange.current;
251
+ if (!target)
252
+ return;
253
+ const range = document.createRange();
254
+ range.setStart(target.node, target.start);
255
+ range.setEnd(target.node, Math.min(target.end, target.node.length));
256
+ range.deleteContents();
257
+ const element = document.createElement('span');
258
+ element.className = 'kpt-mention';
259
+ element.dataset['mention'] = item.id;
260
+ element.setAttribute('contenteditable', 'false');
261
+ element.textContent = '@' + item.label;
262
+ const space = document.createTextNode(' ');
263
+ range.insertNode(space);
264
+ range.insertNode(element);
265
+ const after = document.createRange();
266
+ after.setStartAfter(space);
267
+ after.collapse(true);
268
+ const selection = window.getSelection();
269
+ selection?.removeAllRanges();
270
+ selection?.addRange(after);
271
+ closeMention();
272
+ pushValue();
273
+ onMention?.(item);
274
+ }, [closeMention, onMention, pushValue]);
178
275
  // Stan narzędzi zależy od zaznaczenia, a to zmienia się także poza naszymi zdarzeniami
179
276
  // (myszą, klawiaturą, przez inne okno), więc nasłuchujemy `selectionchange` na dokumencie.
180
277
  useEffect(() => {
181
- const onSelectionChange = () => refreshState();
278
+ const onSelectionChange = () => {
279
+ refreshState();
280
+ // Kursor może odjechać od `@` także bez pisania (klik, strzałki) — wtedy podpowiedzi
281
+ // muszą zniknąć, inaczej Enter wstawiłby wzmiankę w zupełnie innym miejscu.
282
+ syncMention();
283
+ };
182
284
  document.addEventListener('selectionchange', onSelectionChange);
183
285
  return () => document.removeEventListener('selectionchange', onSelectionChange);
184
- }, [refreshState]);
286
+ }, [refreshState, syncMention]);
185
287
  // Przeciąganie krawędzi kolumny i zaznaczanie komórek biegną na całym dokumencie: kursor
186
288
  // wyjeżdża poza tabelę praktycznie zawsze.
187
289
  useEffect(() => {
@@ -344,8 +446,43 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
344
446
  * Tab: wcięcie w liście, przeskok komórki w tabeli. Poza nimi i na krawędziach zostawiamy Tab
345
447
  * natywny — inaczej edytor byłby pułapką klawiaturową (WCAG 2.1.2).
346
448
  */
449
+ /** Klawiatura listy podpowiedzi; `true` = zdarzenie obsłużone i edytor go już nie zobaczy. */
450
+ const onMentionKeyDown = (event) => {
451
+ const count = mentionMatches.length;
452
+ if (!count)
453
+ return false;
454
+ switch (event.key) {
455
+ case 'ArrowDown':
456
+ setMentionActive((index) => (index + 1) % count);
457
+ break;
458
+ case 'ArrowUp':
459
+ setMentionActive((index) => (index - 1 + count) % count);
460
+ break;
461
+ case 'Enter':
462
+ case 'Tab': {
463
+ const item = mentionMatches[mentionActive];
464
+ if (!item)
465
+ return false;
466
+ insertMention(item);
467
+ break;
468
+ }
469
+ case 'Escape':
470
+ closeMention();
471
+ break;
472
+ default:
473
+ return false;
474
+ }
475
+ event.preventDefault();
476
+ return true;
477
+ };
347
478
  const onKeyDown = (event) => {
348
- if (disabled || event.key !== 'Tab' || event.ctrlKey || event.altKey || event.metaKey)
479
+ if (disabled)
480
+ return;
481
+ // Podpowiedzi wzmianek przechwytują klawiaturę jako pierwsze: Tab i Enter znaczą w nich
482
+ // co innego niż w samym edytorze.
483
+ if (mentionOpen && onMentionKeyDown(event))
484
+ return;
485
+ if (event.key !== 'Tab' || event.ctrlKey || event.altKey || event.metaKey)
349
486
  return;
350
487
  if (shiftItems(editorRef.current, event.shiftKey ? 'outdent' : 'indent', maxListLevel)) {
351
488
  event.preventDefault();
@@ -570,7 +707,22 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
570
707
  return (_jsx("button", { ref: setTableAnchor, type: "button", className: cn('kpt-rich-text__tool', tableOpen && 'is-active'), "aria-label": t('richText.table'), title: t('richText.table'), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
571
708
  }
572
709
  return toolButton(item);
573
- })] }, index))) }), sourceView ? (_jsx("textarea", { className: "kpt-rich-text__source", value: current, disabled: disabled, spellCheck: false, "aria-label": t('richText.htmlSource'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() })) : (_jsx("div", { ref: editorRef, className: "kpt-rich-text__editor", contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "aria-invalid": showInvalid || undefined, role: "textbox", "aria-multiline": "true", onInput: pushValue, onKeyDown: onKeyDown, onPaste: onPaste, onContextMenu: onContextMenu, onMouseDown: onEditorMouseDown, onMouseMove: onEditorMouseMove, onBlur: () => onTouch?.() }))] }), _jsx(KptOverlay, { open: styleOpen, anchor: styleAnchor, onClose: () => setStyleOpen(false), offset: 4, children: _jsx("ul", { className: "kpt-rich-text__style-panel", role: "listbox", children: styleOptions.map((option) => (_jsx("li", { className: cn(`kpt-rich-text__style-opt kpt-rich-text__style-opt--${option.tag}`, currentBlock === option.tag && 'is-selected'), role: "option", "aria-selected": currentBlock === option.tag, onClick: () => {
710
+ })] }, index))) }), sourceView ? (_jsx("textarea", { className: "kpt-rich-text__source", value: current, disabled: disabled, spellCheck: false, "aria-label": t('richText.htmlSource'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() })) : (_jsx("div", { ref: editorRef, className: "kpt-rich-text__editor", contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "aria-invalid": showInvalid || undefined, role: "textbox", "aria-multiline": "true", onInput: () => {
711
+ pushValue();
712
+ syncMention();
713
+ }, onKeyDown: onKeyDown, onPaste: onPaste, onContextMenu: onContextMenu, onMouseDown: onEditorMouseDown, onMouseMove: onEditorMouseMove, onBlur: () => onTouch?.() }))] }), mentionOpen && container
714
+ ? createPortal(_jsx("ul", { className: "kpt-rt-mentions", role: "listbox", "aria-label": t('richText.mentions'), style: {
715
+ position: 'fixed',
716
+ left: mentionPoint.x,
717
+ top: mentionPoint.y,
718
+ zIndex: 'var(--kpt-z-overlay, 1100)',
719
+ }, children: mentionMatches.map((item, index) => (_jsxs("li", { className: "kpt-rt-mentions__item", role: "option", "aria-selected": index === mentionActive, "data-active": index === mentionActive ? '' : undefined, onMouseDown: (event) => {
720
+ // `mousedown` zamiast `click`: kliknięcie nie może zabrać fokusu edytorowi,
721
+ // bo razem z nim zniknąłby zakres, w który wstawiamy wzmiankę.
722
+ event.preventDefault();
723
+ insertMention(item);
724
+ }, onMouseEnter: () => setMentionActive(index), children: [_jsx("span", { className: "kpt-rt-mentions__label", children: item.label }), item.kind ? _jsx("span", { className: "kpt-rt-mentions__kind", children: item.kind }) : null] }, item.id))) }), container)
725
+ : null, _jsx(KptOverlay, { open: styleOpen, anchor: styleAnchor, onClose: () => setStyleOpen(false), offset: 4, children: _jsx("ul", { className: "kpt-rich-text__style-panel", role: "listbox", children: styleOptions.map((option) => (_jsx("li", { className: cn(`kpt-rich-text__style-opt kpt-rich-text__style-opt--${option.tag}`, currentBlock === option.tag && 'is-selected'), role: "option", "aria-selected": currentBlock === option.tag, onClick: () => {
574
726
  setStyleOpen(false);
575
727
  focusEditor();
576
728
  restoreSelection();
@@ -13,7 +13,16 @@ const ALLOWED_STYLES = new Set([
13
13
  'color', 'background-color', 'text-align', 'font-weight', 'font-style', 'text-decoration',
14
14
  'width', 'height', 'border', 'border-color', 'border-width', 'border-style', 'vertical-align',
15
15
  ]);
16
- const ALLOWED_ATTRS = new Set(['colspan', 'rowspan', 'span', 'width', 'class', 'target', 'rel', 'title']);
16
+ /*
17
+ * `data-mention` i `contenteditable` są tu dla wzmianek: identyfikator musi przeżyć zapis treści,
18
+ * a `contenteditable="false"` trzyma wzmiankę jako jeden kawałek, w którego środku nie da się
19
+ * pisać. Oba atrybuty są bierne — nie wykonują niczego i nie wskazują zasobów, więc wpuszczenie
20
+ * ich nie otwiera nowej furtki.
21
+ */
22
+ const ALLOWED_ATTRS = new Set([
23
+ 'colspan', 'rowspan', 'span', 'width', 'class', 'target', 'rel', 'title',
24
+ 'data-mention', 'contenteditable',
25
+ ]);
17
26
  // Schematy dopuszczone w `href` narzędzia „link"; blocklista byłaby obchodzalna.
18
27
  const SAFE_URL_SCHEME = /^(https?:|mailto:|tel:)/i;
19
28
  const HAS_URL_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { buildRows, buildTimeline, linkGeometry, resolveLinks, resolveRange, sliceRows, } from '@konce-pt/roadmap';
4
4
  import { cn } from "../internal/cn.js";
5
+ import { isDevBuild } from "../internal/dev.js";
5
6
  import { useControllableState } from "../internal/use-controllable-state.js";
6
7
  import { useKptMessages } from "../i18n/i18n-context.js";
7
8
  import { KptTooltip } from "../tooltip/tooltip.js";
@@ -262,9 +263,6 @@ export function KptRoadmap({ config, scrollHeight = null, labelWidth = '14rem',
262
263
  }, tabIndex: 0, onClick: () => handleItemClick(bar.item), onKeyDown: (event) => handleItemKeydown(event, bar.item), children: [bar.progress !== null && (_jsx("span", { className: "kpt-roadmap__bar-progress", style: { inlineSize: `${bar.progress * 100}%` } })), _jsx("span", { className: "kpt-roadmap__bar-label", children: bar.label })] }) }, bar.item.id))) })] }, row.key));
263
264
  })] }))] }) }));
264
265
  }
265
- function isDevBuild() {
266
- return typeof process !== 'undefined' && process?.env?.['NODE_ENV'] !== 'production';
267
- }
268
266
  /** Identyfikatory grup zwiniętych deklaratywnie w danych. */
269
267
  function declaredCollapsed(groups) {
270
268
  return (groups ?? []).filter((group) => group.collapsed).map((group) => group.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konce-pt/react",
3
- "version": "0.8.2",
3
+ "version": "0.8.5",
4
4
  "description": "Open-source React 19 component library on the Koncept UI design system — the same kpt-* CSS layer and --kpt-* tokens as the Angular port. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "konce.pt",
@@ -42,6 +42,10 @@
42
42
  "types": "./dist/charts-entry/public-api.d.ts",
43
43
  "default": "./dist/charts-entry/public-api.js"
44
44
  },
45
+ "./chat": {
46
+ "types": "./dist/chat-entry/public-api.d.ts",
47
+ "default": "./dist/chat-entry/public-api.js"
48
+ },
45
49
  "./roadmap": {
46
50
  "types": "./dist/roadmap-entry/public-api.d.ts",
47
51
  "default": "./dist/roadmap-entry/public-api.js"
@@ -61,14 +65,17 @@
61
65
  "leaflet": "^1.9.0"
62
66
  },
63
67
  "dependencies": {
64
- "@konce-pt/datetime": "0.8.2",
65
- "@konce-pt/chart": "0.8.2",
66
- "@konce-pt/i18n": "0.8.2",
67
- "@konce-pt/roadmap": "0.8.2",
68
- "@konce-pt/tokens": "0.8.2",
69
- "@konce-pt/table": "0.8.2",
70
- "@konce-pt/icons": "0.8.2",
71
- "@konce-pt/map": "0.8.2"
68
+ "@konce-pt/chart": "0.8.5",
69
+ "@konce-pt/backdrop": "0.8.5",
70
+ "@konce-pt/datetime": "0.8.5",
71
+ "@konce-pt/mention": "0.8.5",
72
+ "@konce-pt/chat": "0.8.5",
73
+ "@konce-pt/icons": "0.8.5",
74
+ "@konce-pt/roadmap": "0.8.5",
75
+ "@konce-pt/table": "0.8.5",
76
+ "@konce-pt/map": "0.8.5",
77
+ "@konce-pt/tokens": "0.8.5",
78
+ "@konce-pt/i18n": "0.8.5"
72
79
  },
73
80
  "devDependencies": {
74
81
  "@types/react": "^19.2.0",