@konce-pt/react 0.8.3 → 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 +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/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
@@ -1,6 +1,7 @@
1
1
  # KptOrderList (@konce-pt/react)
2
2
 
3
- A list with manual reordering. Click selects an item, the buttons move it up and down.
3
+ A list with manual reordering. Click selects an item, the buttons move it up and down, and
4
+ `dragdrop` lets you drag it into place.
4
5
  Import: `import { KptOrderList } from '@konce-pt/react';`
5
6
 
6
7
  ## DOM
@@ -19,23 +20,33 @@ Import: `import { KptOrderList } from '@konce-pt/react';`
19
20
  ## Props
20
21
  - `items` / `defaultItems` / `onItemsChange` — the list; the callback receives the new order
21
22
  - `header`: string
23
+ - `dragdrop` — `false` by default; enables dragging (opt-in, nothing changes without it)
22
24
  - `renderItem(item, index)` — required; the equivalent of `<ng-template let-item>`
23
25
  - `className`, `ref` and every other `<div>` prop go to the host
24
26
 
25
27
  ## The selection follows the item
26
28
  After a move the selection jumps to the new index, so pressing the button again moves the same
27
- entry — with the selection pinned to a position, the second press would move its neighbour.
29
+ entry — with the selection pinned to a position, the second press would move its neighbour. A drop
30
+ does the same.
31
+
32
+ ## Where a dragged item lands
33
+ Dropping onto a row puts the dragged item at that row's position and everything else shifts, while
34
+ the buttons keep swapping with the neighbour. Dropping on the empty part of the list appends at the
35
+ end. While a drag is running the row under the cursor carries a `--dragover` modifier and the
36
+ dragged one `is-dragging`.
28
37
 
29
38
  ## Examples
30
- <KptOrderList items={tasks} onItemsChange={setTasks} header="Order" renderItem={(item) => item} />
39
+ <KptOrderList items={tasks} onItemsChange={setTasks} header="Order" dragdrop renderItem={(item) => item} />
31
40
  <KptOrderList defaultItems={steps} renderItem={(step) => step.name} />
32
41
 
33
42
  ## i18n
34
- The buttons use `orderList.up` and `orderList.down`.
43
+ The buttons use `orderList.up` and `orderList.down`. Dragging carries no visible label, so it adds
44
+ no dictionary keys.
35
45
 
36
46
  ## Tokens
37
47
  The selection `--kpt-color-primary`, the buttons `--kpt-color-surface-raised`.
38
48
 
39
49
  ## Accessibility
40
50
  Both buttons are labelled from the dictionary and go disabled while nothing is selected, so the
41
- control never looks broken.
51
+ control never looks broken. Native HTML5 drag and drop has no keyboard equivalent, so they stay the
52
+ full keyboard path — `dragdrop` never replaces them.
@@ -5,14 +5,16 @@ export interface KptOrderListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
5
5
  defaultItems?: T[];
6
6
  onItemsChange?: (items: T[]) => void;
7
7
  header?: string;
8
+ /** Włącza przestawianie pozycji metodą przeciągnij-i-upuść. */
9
+ dragdrop?: boolean;
8
10
  /** Szablon elementu — odpowiednik `<ng-template let-item>` z Angulara. */
9
11
  renderItem: (item: T, index: number) => ReactNode;
10
12
  }
11
13
  /**
12
14
  * OrderList — lista z ręcznym porządkowaniem. Klik zaznacza element, przyciski przesuwają go
13
- * w górę i w dół.
15
+ * w górę i w dół. Z `dragdrop` pozycje przestawia się także przeciąganiem.
14
16
  *
15
17
  * @example
16
- * <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" renderItem={(t) => t.name} />
18
+ * <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" dragdrop renderItem={(t) => t.name} />
17
19
  */
18
- export declare function KptOrderList<T>({ items, defaultItems, onItemsChange, header, renderItem, locale, dictionary, className, ...rest }: Readonly<KptOrderListProps<T>>): import("react").JSX.Element;
20
+ export declare function KptOrderList<T>({ items, defaultItems, onItemsChange, header, dragdrop, renderItem, locale, dictionary, className, ...rest }: Readonly<KptOrderListProps<T>>): import("react").JSX.Element;
@@ -7,12 +7,12 @@ import { KptIcon } from "../icon/icon.js";
7
7
  const EMPTY = [];
8
8
  /**
9
9
  * OrderList — lista z ręcznym porządkowaniem. Klik zaznacza element, przyciski przesuwają go
10
- * w górę i w dół.
10
+ * w górę i w dół. Z `dragdrop` pozycje przestawia się także przeciąganiem.
11
11
  *
12
12
  * @example
13
- * <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" renderItem={(t) => t.name} />
13
+ * <KptOrderList items={tasks} onItemsChange={setTasks} header="Kolejność" dragdrop renderItem={(t) => t.name} />
14
14
  */
15
- export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, header = '', renderItem, locale, dictionary, className, ...rest }) {
15
+ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, header = '', dragdrop = false, renderItem, locale, dictionary, className, ...rest }) {
16
16
  const { t } = useKptMessages({ locale, dictionary });
17
17
  const [list, setList] = useControllableState({
18
18
  value: items,
@@ -20,6 +20,12 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
20
20
  onChange: onItemsChange,
21
21
  });
22
22
  const [selected, setSelected] = useState(null);
23
+ /** Indeks przeciąganej pozycji; `null` gdy nic nie jest przeciągane. */
24
+ const [dragIndex, setDragIndex] = useState(null);
25
+ /** Indeks pozycji pod kursorem — rysuje wskaźnik miejsca upuszczenia. */
26
+ const [overIndex, setOverIndex] = useState(null);
27
+ /** Kursor nad pustym obszarem listy — upuszczenie tam dopina na koniec. */
28
+ const [listOver, setListOver] = useState(false);
23
29
  const move = (dir) => {
24
30
  if (selected === null)
25
31
  return;
@@ -33,5 +39,64 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
33
39
  // przesuwałoby sąsiada zamiast tego samego wpisu.
34
40
  setSelected(target);
35
41
  };
36
- return (_jsx("div", { className: cn('kpt-order-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-order-list", children: [_jsxs("div", { className: "kpt-order-list__controls", children: [_jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.up'), onClick: () => move(-1), children: _jsx(KptIcon, { name: "arrow-up" }) }), _jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.down'), onClick: () => move(1), children: _jsx(KptIcon, { name: "arrow-down" }) })] }), _jsxs("div", { className: "kpt-order-list__panel", children: [header ? _jsx("div", { className: "kpt-order-list__header", children: header }) : null, _jsx("ul", { className: "kpt-order-list__items", children: list.map((item, index) => (_jsx("li", { className: cn('kpt-order-list__item', selected === index && 'is-selected'), onClick: () => setSelected(index), children: renderItem(item, index) }, index))) })] })] }) }));
42
+ const clearDrag = () => {
43
+ setDragIndex(null);
44
+ setOverIndex(null);
45
+ setListOver(false);
46
+ };
47
+ const onDragStart = (index, event) => {
48
+ if (!dragdrop)
49
+ return;
50
+ setDragIndex(index);
51
+ event.dataTransfer.setData('text/plain', String(index));
52
+ event.dataTransfer.effectAllowed = 'move';
53
+ };
54
+ const onDragOver = (index, event) => {
55
+ if (dragIndex === null)
56
+ return;
57
+ // Zatrzymujemy propagację, żeby `<ul>` nie uznał, że kursor jest nad pustym obszarem.
58
+ event.preventDefault();
59
+ event.stopPropagation();
60
+ setOverIndex(index);
61
+ setListOver(false);
62
+ };
63
+ const onDrop = (index, event) => {
64
+ event.preventDefault();
65
+ event.stopPropagation();
66
+ const from = dragIndex;
67
+ clearDrag();
68
+ if (from === null || from === index)
69
+ return;
70
+ // Indeks celu liczony na tablicy sprzed usunięcia — element ląduje na pozycji tego,
71
+ // na który go upuszczono (ten sam wzór co przestawianie kolumn w KptDataTable).
72
+ const next = [...list];
73
+ const [moved] = next.splice(from, 1);
74
+ if (moved === undefined)
75
+ return;
76
+ next.splice(index, 0, moved);
77
+ setList(next);
78
+ setSelected(index);
79
+ };
80
+ const onListDragOver = (event) => {
81
+ if (dragIndex === null)
82
+ return;
83
+ event.preventDefault();
84
+ setListOver(true);
85
+ setOverIndex(null);
86
+ };
87
+ const onListDrop = (event) => {
88
+ event.preventDefault();
89
+ const from = dragIndex;
90
+ clearDrag();
91
+ if (from === null)
92
+ return;
93
+ const next = [...list];
94
+ const [moved] = next.splice(from, 1);
95
+ if (moved === undefined)
96
+ return;
97
+ next.push(moved);
98
+ setList(next);
99
+ setSelected(next.length - 1);
100
+ };
101
+ return (_jsx("div", { className: cn('kpt-order-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-order-list", children: [_jsxs("div", { className: "kpt-order-list__controls", children: [_jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.up'), onClick: () => move(-1), children: _jsx(KptIcon, { name: "arrow-up" }) }), _jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.down'), onClick: () => move(1), children: _jsx(KptIcon, { name: "arrow-down" }) })] }), _jsxs("div", { className: "kpt-order-list__panel", children: [header ? _jsx("div", { className: "kpt-order-list__header", children: header }) : null, _jsx("ul", { className: cn('kpt-order-list__items', listOver && 'is-dragover'), onDragOver: onListDragOver, onDragLeave: () => setListOver(false), onDrop: onListDrop, children: list.map((item, index) => (_jsx("li", { className: cn('kpt-order-list__item', selected === index && 'is-selected', dragIndex === index && 'is-dragging', dragdrop && 'kpt-order-list__item--draggable', overIndex === index && dragIndex !== index && 'kpt-order-list__item--dragover'), draggable: dragdrop || undefined, onClick: () => setSelected(index), onDragStart: (event) => onDragStart(index, event), onDragOver: (event) => onDragOver(index, event), onDragLeave: () => setOverIndex(null), onDrop: (event) => onDrop(index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index))) })] })] }) }));
37
102
  }
@@ -0,0 +1,42 @@
1
+ # KptPeek (@konce-pt/react)
2
+
3
+ A peek card — content tied to what you are looking at, shown without leaving the text. Hover opens
4
+ it, the card itself is interactive, and a click pins it.
5
+ Import: `import { KptPeek } from '@konce-pt/react';`
6
+
7
+ ## DOM
8
+ <span class="kpt-peek-host">
9
+ <a class="kpt-peek__trigger" aria-expanded="true" aria-controls="…">…trigger…</a>
10
+ </span>
11
+ <!-- while open, in a portal on document.body: -->
12
+ <div class="kpt-peek__panel" role="dialog" tabindex="-1" data-pinned>…children…</div>
13
+
14
+ ## Props
15
+ - `trigger`: ReactNode — the element the card hangs on; a mention, an avatar, a chip, a link
16
+ - `children`: ReactNode — the card content
17
+ - `placement`: 'top' | 'bottom' (default 'bottom')
18
+ - `openDelay`: number (default 300), `closeDelay`: number (default 200)
19
+ - `disabled`: boolean — the card never opens; the trigger stays a plain link or button
20
+ - `label`: string — the card's accessible name; without it the trigger's text is used
21
+ - `onOpenChange`: `(open: boolean) => void` — the moment to fetch the card's data
22
+ - `className`, `ref` and every other `<span>` prop go to the host
23
+
24
+ ## The props land on the trigger itself
25
+ A valid element passed as `trigger` is cloned with the handlers and `aria-expanded` merged in, so
26
+ the state sits on the element a screen reader actually reads — not on some `<span>` next to it.
27
+ Anything else (a bare string) gets wrapped in a `<span>`.
28
+
29
+ ## Hover is not the only way in
30
+ Hover opens after `openDelay` and leaving closes after `closeDelay` — the pointer needs time to
31
+ travel from the trigger to the card, so entering the card cancels the close. Focus opens it with no
32
+ delay. A click, Enter or a tap **pins** the card: it stays after the pointer leaves and focus moves
33
+ inside it. That is the only route to the card's content where there is no hover at all. Escape
34
+ closes it and hands focus back to the trigger; so does a pointer press outside.
35
+
36
+ ## Which of the three
37
+ `KptTooltip` carries plain text and cannot be clicked. `KptPopover` opens on click and puts a
38
+ backdrop over the page. `KptPeek` opens on hover, the card is interactive, and nothing is covered.
39
+
40
+ ## Tokens
41
+ `--kpt-color-surface-raised`, `--kpt-color-border`, `--kpt-elevation-3`, `--kpt-color-focus-ring`,
42
+ `--kpt-z-overlay`.
@@ -0,0 +1,35 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ /** Z której strony wyzwalacza wychodzi karta. */
3
+ export type KptPeekPlacement = 'top' | 'bottom';
4
+ export interface KptPeekProps extends Omit<ComponentPropsWithRef<'span'>, 'content'> {
5
+ /** Element, na którym wisi karta — wzmianka, awatar, chip albo zwykły link. */
6
+ trigger: ReactNode;
7
+ /** Treść karty. */
8
+ children?: ReactNode;
9
+ placement?: KptPeekPlacement;
10
+ /** Zwłoka otwarcia po najechaniu (ms) — bez niej karty mrugają przy przejeżdżaniu przez tekst. */
11
+ openDelay?: number;
12
+ /** Zwłoka zamknięcia po zjeździe kursora (ms) — tyle jest na przejazd z wyzwalacza na kartę. */
13
+ closeDelay?: number;
14
+ /** Wyłączona karta nie otwiera się wcale; wyzwalacz zostaje zwykłym linkiem czy przyciskiem. */
15
+ disabled?: boolean;
16
+ /** Nazwa karty dla czytnika ekranu; bez niej bierze się tekst wyzwalacza. */
17
+ label?: string;
18
+ onOpenChange?: (open: boolean) => void;
19
+ }
20
+ /**
21
+ * Karta podglądu pod kursorem — treść powiązana z tym, na co się patrzy, bez opuszczania tekstu.
22
+ * Tym różni się od sąsiadów: `KptTooltip` niesie sam tekst i nie da się w nim kliknąć,
23
+ * a `KptPopover` otwiera dopiero klik i zasłania stronę backdropem.
24
+ *
25
+ * Hover pokazuje kartę po `openDelay`, zjazd kursora chowa ją po `closeDelay` — kursor musi mieć
26
+ * czas przejechać z wyzwalacza na kartę, więc najazd na kartę anuluje zamknięcie. Fokus otwiera
27
+ * kartę od razu, a klik, Enter albo dotyk **przypina** ją: karta zostaje mimo zjazdu kursora,
28
+ * a fokus wchodzi do środka. To jedyna droga do treści karty tam, gdzie hovera nie ma w ogóle.
29
+ *
30
+ * @example
31
+ * <KptPeek trigger={<a href="/ludzie/anna">@Anna Kowalska</a>}>
32
+ * <KptCard>Profil, projekty, kontakt…</KptCard>
33
+ * </KptPeek>
34
+ */
35
+ export declare function KptPeek({ trigger, children, placement, openDelay, closeDelay, disabled, label, onOpenChange, className, ...rest }: Readonly<KptPeekProps>): import("react").JSX.Element;
@@ -0,0 +1,127 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cloneElement, isValidElement, useCallback, useEffect, useId, useRef, useState, } from 'react';
3
+ import { cn } from "../internal/cn.js";
4
+ import { KptOverlay } from "../internal/overlay.js";
5
+ import { mergeSlotProps } from "../internal/slot.js";
6
+ /**
7
+ * Karta podglądu pod kursorem — treść powiązana z tym, na co się patrzy, bez opuszczania tekstu.
8
+ * Tym różni się od sąsiadów: `KptTooltip` niesie sam tekst i nie da się w nim kliknąć,
9
+ * a `KptPopover` otwiera dopiero klik i zasłania stronę backdropem.
10
+ *
11
+ * Hover pokazuje kartę po `openDelay`, zjazd kursora chowa ją po `closeDelay` — kursor musi mieć
12
+ * czas przejechać z wyzwalacza na kartę, więc najazd na kartę anuluje zamknięcie. Fokus otwiera
13
+ * kartę od razu, a klik, Enter albo dotyk **przypina** ją: karta zostaje mimo zjazdu kursora,
14
+ * a fokus wchodzi do środka. To jedyna droga do treści karty tam, gdzie hovera nie ma w ogóle.
15
+ *
16
+ * @example
17
+ * <KptPeek trigger={<a href="/ludzie/anna">@Anna Kowalska</a>}>
18
+ * <KptCard>Profil, projekty, kontakt…</KptCard>
19
+ * </KptPeek>
20
+ */
21
+ export function KptPeek({ trigger, children, placement = 'bottom', openDelay = 300, closeDelay = 200, disabled = false, label, onOpenChange, className, ...rest }) {
22
+ const hostRef = useRef(null);
23
+ const triggerRef = useRef(null);
24
+ const panelRef = useRef(null);
25
+ const timer = useRef(undefined);
26
+ const [open, setOpen] = useState(false);
27
+ /** Przypięta karta zostaje po zjeździe kursora — przypina ją klik, Enter albo dotyk. */
28
+ const [pinned, setPinned] = useState(false);
29
+ const panelId = useId();
30
+ const stop = useCallback(() => {
31
+ if (timer.current)
32
+ clearTimeout(timer.current);
33
+ timer.current = undefined;
34
+ }, []);
35
+ const schedule = useCallback((action, delay) => {
36
+ stop();
37
+ if (delay <= 0)
38
+ action();
39
+ else
40
+ timer.current = setTimeout(action, delay);
41
+ }, [stop]);
42
+ const show = useCallback(() => {
43
+ setOpen((was) => {
44
+ if (!was)
45
+ onOpenChange?.(true);
46
+ return true;
47
+ });
48
+ }, [onOpenChange]);
49
+ const close = useCallback(() => {
50
+ stop();
51
+ setPinned(false);
52
+ setOpen((was) => {
53
+ if (was)
54
+ onOpenChange?.(false);
55
+ return false;
56
+ });
57
+ }, [onOpenChange, stop]);
58
+ const hoverIn = useCallback(() => {
59
+ if (disabled)
60
+ return;
61
+ schedule(show, open ? 0 : openDelay);
62
+ }, [disabled, open, openDelay, schedule, show]);
63
+ const hoverOut = useCallback(() => {
64
+ if (pinned)
65
+ return;
66
+ schedule(close, closeDelay);
67
+ }, [close, closeDelay, pinned, schedule]);
68
+ const focusIn = useCallback(() => {
69
+ if (disabled)
70
+ return;
71
+ schedule(show, 0);
72
+ }, [disabled, schedule, show]);
73
+ const focusOut = useCallback(() => {
74
+ // Sprawdzenie po klatce: w chwili `blur` aktywnym elementem jest jeszcze `body`, więc
75
+ // przejście Tabem z wyzwalacza do karty wyglądałoby jak wyjście na zewnątrz.
76
+ schedule(() => {
77
+ const active = document.activeElement;
78
+ if (hostRef.current?.contains(active) || panelRef.current?.contains(active))
79
+ return;
80
+ close();
81
+ }, 0);
82
+ }, [close, schedule]);
83
+ /** Klik, Enter albo dotyk: karta zostaje i przyjmuje fokus. */
84
+ const pin = useCallback(() => {
85
+ if (disabled)
86
+ return;
87
+ setPinned(true);
88
+ show();
89
+ // Panel powstaje dopiero po przerysowaniu portalu, więc fokus idzie w następnej klatce.
90
+ setTimeout(() => panelRef.current?.focus({ preventScroll: true }));
91
+ }, [disabled, show]);
92
+ useEffect(() => stop, [stop]);
93
+ // Przypiętej karty nie zamyka zjazd kursora, więc kliknięcie obok musi ją zdjąć. Faza
94
+ // przechwytywania i `pointerdown`: własne `onClick` w karcie zdąży się wykonać wcześniej.
95
+ useEffect(() => {
96
+ if (!open)
97
+ return;
98
+ const outside = (event) => {
99
+ const target = event.target;
100
+ if (hostRef.current?.contains(target) || panelRef.current?.contains(target))
101
+ return;
102
+ close();
103
+ };
104
+ document.addEventListener('pointerdown', outside, true);
105
+ return () => document.removeEventListener('pointerdown', outside, true);
106
+ }, [close, open]);
107
+ const triggerProps = {
108
+ className: 'kpt-peek__trigger',
109
+ ref: triggerRef,
110
+ onMouseEnter: hoverIn,
111
+ onMouseLeave: hoverOut,
112
+ onFocus: focusIn,
113
+ onBlur: focusOut,
114
+ onClick: pin,
115
+ 'aria-expanded': open,
116
+ 'aria-controls': open ? panelId : undefined,
117
+ };
118
+ return (_jsxs("span", { ref: hostRef, className: cn('kpt-peek-host', className), ...rest, children: [isValidElement(trigger) ? (
119
+ // Propsy idą na sam wyzwalacz, nie na opakowanie: `aria-expanded` ma siedzieć na tym
120
+ // elemencie, po którym jeździ czytnik ekranu, a nie na obojętnym `<span>` obok.
121
+ cloneElement(trigger, mergeSlotProps(triggerProps, trigger.props))) : (_jsx("span", { ...triggerProps, children: trigger })), _jsx(KptOverlay, { open: open, anchor: triggerRef.current, onClose: close, offset: 8, withBackdrop: false, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-peek__panel", role: "dialog", tabIndex: -1, "aria-label": label || triggerRef.current?.textContent?.trim() || undefined, "data-pinned": pinned ? '' : undefined, onMouseEnter: hoverIn, onMouseLeave: hoverOut, onFocus: focusIn, onBlur: focusOut, onKeyDown: (event) => {
122
+ if (event.key !== 'Escape')
123
+ return;
124
+ close();
125
+ triggerRef.current?.focus({ preventScroll: true });
126
+ }, children: children }) })] }));
127
+ }
@@ -1,7 +1,7 @@
1
1
  # KptPickList (@konce-pt/react)
2
2
 
3
3
  Transfers items between two lists. Click to select, the middle buttons move the selection or
4
- everything.
4
+ everything, a double-click sends one row across, and `dragdrop` adds dragging.
5
5
  Import: `import { KptPickList } from '@konce-pt/react';`
6
6
 
7
7
  ## DOM
@@ -22,13 +22,24 @@ Import: `import { KptPickList } from '@konce-pt/react';`
22
22
  - `source` / `defaultSource` / `onSourceChange` — the left list
23
23
  - `target` / `defaultTarget` / `onTargetChange` — the right list
24
24
  - `sourceHeader`, `targetHeader` — without them `pickList.available` / `pickList.selected`
25
+ - `dragdrop` — `false` by default; enables dragging (opt-in, nothing changes without it)
25
26
  - `renderItem(item, index)` — required; the equivalent of `<ng-template let-item>`
26
27
  - `className`, `ref` and every other `<div>` prop go to the host
27
28
 
28
29
  ## The selection is kept by reference
29
30
  Selected items live in a `Set` of references, not of indexes: after a move the indexes on both
30
- sides shift, while a reference stays the same. That is also why a moved item never carries its
31
- selection over — the selection is cleared on every move.
31
+ sides shift, while a reference stays the same. The buttons clear the whole selection of the side
32
+ they empty; a double-click or a drag only drops the one item that left.
33
+
34
+ ## A double-click moves one item
35
+ It sends exactly the row you clicked to the other panel and appends it at the end, whatever else is
36
+ selected. It does not need `dragdrop` — the two clicks underneath cancel each other out, so the rest
37
+ of the selection is left alone.
38
+
39
+ ## Where a dragged item lands
40
+ Dropping onto a row puts the dragged item at that row's position: inside one panel that reorders,
41
+ across panels it transfers. Dropping on the empty part of a panel appends at the end. While a drag
42
+ is running the row under the cursor carries a `--dragover` modifier and the dragged one `is-dragging`.
32
43
 
33
44
  ## Two lists, two controlled values
34
45
  Both sides are independent controllable values, so the component works uncontrolled (`default*`),
@@ -38,15 +49,18 @@ half-controlled, or fully controlled by the form.
38
49
  <KptPickList
39
50
  source={available} onSourceChange={setAvailable}
40
51
  target={selected} onTargetChange={setSelected}
52
+ dragdrop
41
53
  renderItem={(item) => item.name}
42
54
  />
43
55
 
44
56
  ## i18n
45
- The headers and the four move buttons come from the `pickList` namespace.
57
+ The headers and the four move buttons come from the `pickList` namespace. Dragging and the
58
+ double-click carry no visible label, so they add no dictionary keys.
46
59
 
47
60
  ## Tokens
48
61
  The selection `--kpt-color-primary`, the headers `--kpt-color-surface-variant`.
49
62
 
50
63
  ## Accessibility
51
64
  Every move button is labelled from the dictionary, so "move all right" is distinguishable from
52
- "move right" without seeing the double chevron.
65
+ "move right" without seeing the double chevron. Native HTML5 drag and drop has no keyboard
66
+ equivalent, so those buttons stay the full keyboard path — `dragdrop` never replaces them.
@@ -11,18 +11,23 @@ export interface KptPickListProps<T> extends Omit<ComponentPropsWithRef<'div'>,
11
11
  sourceHeader?: string;
12
12
  /** Nagłówek listy docelowej; bez niego `pickList.selected`. */
13
13
  targetHeader?: string;
14
+ /** Włącza porządkowanie i przenoszenie elementów metodą przeciągnij-i-upuść. */
15
+ dragdrop?: boolean;
14
16
  /** Szablon elementu — odpowiednik `<ng-template let-item>` z Angulara. */
15
17
  renderItem: (item: T, index: number) => ReactNode;
16
18
  }
17
19
  /**
18
20
  * PickList — transfer elementów między dwiema listami. Klik zaznacza, przyciski pośrodku
19
- * przenoszą zaznaczone albo wszystko.
21
+ * przenoszą zaznaczone albo wszystko, dwuklik przerzuca pojedynczy element na drugą stronę.
22
+ * Z `dragdrop` dochodzi przeciąganie — porządkowanie wewnątrz panelu i przenoszenie na
23
+ * konkretną pozycję drugiego panelu.
20
24
  *
21
25
  * @example
22
26
  * <KptPickList
23
27
  * source={available} onSourceChange={setAvailable}
24
28
  * target={selected} onTargetChange={setSelected}
29
+ * dragdrop
25
30
  * renderItem={(item) => item.name}
26
31
  * />
27
32
  */
28
- export declare function KptPickList<T>({ source, defaultSource, onSourceChange, target, defaultTarget, onTargetChange, sourceHeader, targetHeader, renderItem, locale, dictionary, className, ...rest }: Readonly<KptPickListProps<T>>): import("react").JSX.Element;
33
+ export declare function KptPickList<T>({ source, defaultSource, onSourceChange, target, defaultTarget, onTargetChange, sourceHeader, targetHeader, dragdrop, renderItem, locale, dictionary, className, ...rest }: Readonly<KptPickListProps<T>>): import("react").JSX.Element;
@@ -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
  }