@konce-pt/react 0.8.2 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +11 -4
  2. package/dist/backdrop/backdrop.d.ts +79 -0
  3. package/dist/backdrop/backdrop.js +195 -0
  4. package/dist/backdrop/llms.txt +132 -0
  5. package/dist/chat/chat.d.ts +53 -0
  6. package/dist/chat/chat.js +160 -0
  7. package/dist/chat/llms.txt +78 -0
  8. package/dist/chat-composer/chat-composer.d.ts +41 -0
  9. package/dist/chat-composer/chat-composer.js +81 -0
  10. package/dist/chat-composer/llms.txt +56 -0
  11. package/dist/chat-dock/chat-dock.d.ts +40 -0
  12. package/dist/chat-dock/chat-dock.js +67 -0
  13. package/dist/chat-dock/llms.txt +71 -0
  14. package/dist/chat-entry/public-api.d.ts +12 -0
  15. package/dist/chat-entry/public-api.js +10 -0
  16. package/dist/chat-message/chat-message.d.ts +53 -0
  17. package/dist/chat-message/chat-message.js +112 -0
  18. package/dist/chat-message/llms.txt +73 -0
  19. package/dist/chat-typing/chat-typing.d.ts +23 -0
  20. package/dist/chat-typing/chat-typing.js +30 -0
  21. package/dist/chat-typing/llms.txt +41 -0
  22. package/dist/chip/chip.d.ts +10 -1
  23. package/dist/chip/chip.js +3 -2
  24. package/dist/chip/llms.txt +9 -0
  25. package/dist/data-view/data-view.d.ts +15 -3
  26. package/dist/data-view/data-view.js +22 -5
  27. package/dist/data-view/llms.txt +20 -7
  28. package/dist/internal/dev.d.ts +1 -0
  29. package/dist/internal/dev.js +3 -0
  30. package/dist/internal/overlay.d.ts +6 -1
  31. package/dist/internal/overlay.js +5 -3
  32. package/dist/mention/llms.txt +60 -0
  33. package/dist/mention/mention.d.ts +38 -0
  34. package/dist/mention/mention.js +29 -0
  35. package/dist/mention/mentions.d.ts +31 -0
  36. package/dist/mention/mentions.js +24 -0
  37. package/dist/order-list/llms.txt +16 -5
  38. package/dist/order-list/order-list.d.ts +5 -3
  39. package/dist/order-list/order-list.js +69 -4
  40. package/dist/peek/llms.txt +42 -0
  41. package/dist/peek/peek.d.ts +35 -0
  42. package/dist/peek/peek.js +127 -0
  43. package/dist/pick-list/llms.txt +19 -5
  44. package/dist/pick-list/pick-list.d.ts +7 -2
  45. package/dist/pick-list/pick-list.js +123 -17
  46. package/dist/public-api.d.ts +13 -1
  47. package/dist/public-api.js +6 -0
  48. package/dist/rich-text/llms.txt +10 -0
  49. package/dist/rich-text/rich-text.d.ts +11 -1
  50. package/dist/rich-text/rich-text.js +157 -5
  51. package/dist/rich-text/sanitize.js +10 -1
  52. package/dist/roadmap/roadmap.js +1 -3
  53. package/package.json +16 -9
@@ -0,0 +1,60 @@
1
+ # KptMention / KptMentions (@konce-pt/react)
2
+
3
+ Mentions in text: `@someone` as an element you can hover to see a peek card with the rest of the
4
+ connections. `KptMention` is a single mention; `KptMentions` turns a whole paragraph into text plus
5
+ mentions.
6
+ Import: `import { KptMention, KptMentions } from '@konce-pt/react';`
7
+
8
+ ## DOM
9
+ <span class="kpt-mentions-host">
10
+ <span class="kpt-mentions__text">A question for </span>
11
+ <span class="kpt-peek-host kpt-mention-host">
12
+ <button class="kpt-mention" data-kind="person">@Anna Kowalska</button>
13
+ </span>
14
+ <span class="kpt-mentions__text"> about the estimate.</span>
15
+ </span>
16
+
17
+ ## KptMention — props
18
+ - `item`: `KptMentionItem` — the whole entry at once; the single props below win over it
19
+ - `id`, `label`, `kind`: string — `kind` lands in `data-kind`, so it can be styled per type
20
+ - `prefix`: string (default '@'), `href`: string | null — with `href` the mention is a link
21
+ - `placement`, `openDelay` — passed to `KptPeek`
22
+ - `disabled`: boolean — no card; the mention stays a plain highlighted word
23
+ - `children`: ReactNode — the card
24
+
25
+ ## KptMentions — props
26
+ - `text`: string — the content with `@id` or `@[Name with spaces](id)`
27
+ - `items`: `readonly KptMentionItem[]` — entries the ids resolve against
28
+ - `prefix`, `placement` — passed to every mention
29
+ - `renderCard`: `(item, id, label) => ReactNode` — the card; without it mentions are plain words
30
+
31
+ ## Example
32
+ <KptMentions
33
+ text={note}
34
+ items={people}
35
+ renderCard={(item, id, label) => <KptCard>{item?.data?.role ?? label}</KptCard>}
36
+ />
37
+
38
+ ## The text stays text
39
+ `KptMentions` does not render HTML: the paragraph goes in as a plain string, `parseMentionText()`
40
+ from `@konce-pt/mention` splits it into segments and the mention is an element the library builds
41
+ itself. An application does not have to push user-written content through a sanitizer to get
42
+ mentions out of it.
43
+
44
+ An id that is not on the list still renders — the label falls back to the id, because a note has to
45
+ stay readable after someone leaves the team. Without `renderCard` the mention is a highlighted
46
+ word, not a button: there is nothing to open, so it does not pretend otherwise.
47
+
48
+ ## Mentions in the editor
49
+ `KptRichText` takes `mentions`, `mentionLimit` and `onMention`: typing `@` opens the suggestion
50
+ list, arrows and Enter (or Tab) insert the mention as
51
+ `<span class="kpt-mention" data-mention="id" contenteditable="false">`. The id survives saving, and
52
+ `contenteditable="false"` keeps the mention in one piece.
53
+
54
+ ## Tokens
55
+ The mention is underlined with a **dotted** line: `--kpt-color-primary-border` at rest, darkening
56
+ to `--kpt-color-primary` on hover and while the card is open. Dotted, not solid — a solid underline
57
+ would read as a link, and a mention usually leads nowhere, it opens a card. Text
58
+ `--kpt-color-primary`; a mention without a card `--kpt-color-text` + `--kpt-font-weight-medium` and
59
+ no line at all, since there is nothing to promise. Focus ring `--kpt-color-focus-ring`. The card
60
+ takes its own tokens from `KptPeek`.
@@ -0,0 +1,38 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from 'react';
2
+ import type { KptMentionItem } from '@konce-pt/mention';
3
+ import { type KptPeekPlacement } from '../peek/peek.tsx';
4
+ export interface KptMentionProps extends Omit<ComponentPropsWithRef<'span'>, 'id' | 'prefix' | 'children'> {
5
+ /** Cała pozycja naraz. Pojedyncze propsy poniżej mają nad nią priorytet. */
6
+ item?: KptMentionItem | null;
7
+ /** Identyfikator zapisany w treści — nie zmienia się przy zmianie nazwy. */
8
+ id?: string;
9
+ /** Nazwa do pokazania; bez niej bierze się ją z `item`, a w ostateczności z `id`. */
10
+ label?: string;
11
+ /** Rodzaj (osoba, zadanie, dokument) — ląduje w `data-kind`, więc da się go ostylować. */
12
+ kind?: string;
13
+ /** Znak przed nazwą. Pusty łańcuch zostawia samą nazwę. */
14
+ prefix?: string;
15
+ /** Adres, pod który prowadzi klik. Bez niego wzmianka jest przyciskiem otwierającym kartę. */
16
+ href?: string | null;
17
+ placement?: KptPeekPlacement;
18
+ openDelay?: number;
19
+ /** Wyłączona wzmianka nie otwiera karty — zostaje samym napisem. */
20
+ disabled?: boolean;
21
+ /** Treść karty. Bez niej wzmianka też zostaje napisem: nie ma czego pokazać. */
22
+ children?: ReactNode;
23
+ }
24
+ /**
25
+ * Wzmianka w tekście — `@ktoś` albo `@coś` jako element, na który da się najechać i zobaczyć
26
+ * kartę podglądu z resztą powiązań. Treść karty daje aplikacja przez `children`: biblioteka nie
27
+ * wie, czym jest wzmiankowana rzecz, a osoba, zadanie i dokument wyglądają inaczej.
28
+ *
29
+ * Wyzwalacz jest `<button>`, a nie `<span>`: karta otwiera się też z klawiatury i dotyku
30
+ * (przypięcie), a to wymaga elementu, na który da się wejść Tabem. Z `href` staje się linkiem,
31
+ * bo wtedy klik prowadzi gdzieś naprawdę.
32
+ *
33
+ * @example
34
+ * <KptMention id="anna.k" label="Anna Kowalska">
35
+ * <KptCard>Profil, projekty, kontakt…</KptCard>
36
+ * </KptMention>
37
+ */
38
+ export declare function KptMention({ item, id, label, kind, prefix, href, placement, openDelay, disabled, children, className, ...rest }: Readonly<KptMentionProps>): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "../internal/cn.js";
3
+ import { KptPeek } from "../peek/peek.js";
4
+ /**
5
+ * Wzmianka w tekście — `@ktoś` albo `@coś` jako element, na który da się najechać i zobaczyć
6
+ * kartę podglądu z resztą powiązań. Treść karty daje aplikacja przez `children`: biblioteka nie
7
+ * wie, czym jest wzmiankowana rzecz, a osoba, zadanie i dokument wyglądają inaczej.
8
+ *
9
+ * Wyzwalacz jest `<button>`, a nie `<span>`: karta otwiera się też z klawiatury i dotyku
10
+ * (przypięcie), a to wymaga elementu, na który da się wejść Tabem. Z `href` staje się linkiem,
11
+ * bo wtedy klik prowadzi gdzieś naprawdę.
12
+ *
13
+ * @example
14
+ * <KptMention id="anna.k" label="Anna Kowalska">
15
+ * <KptCard>Profil, projekty, kontakt…</KptCard>
16
+ * </KptMention>
17
+ */
18
+ export function KptMention({ item = null, id = '', label = '', kind = '', prefix = '@', href = null, placement = 'bottom', openDelay = 300, disabled = false, children, className, ...rest }) {
19
+ const display = prefix + (label || item?.label || id || item?.id || '');
20
+ const resolvedKind = kind || item?.kind || undefined;
21
+ const plain = disabled || !children;
22
+ if (plain) {
23
+ // Bez karty wzmianka nie udaje kontrolki: zostaje wyróżnionym napisem, którego nie ma
24
+ // po co dotykać Tabem.
25
+ return (_jsx("span", { className: cn('kpt-mention-host', className), ...rest, children: _jsx("span", { className: "kpt-mention", "data-plain": "", "data-kind": resolvedKind, children: display }) }));
26
+ }
27
+ const trigger = href ? (_jsx("a", { className: "kpt-mention", href: href, "data-kind": resolvedKind, children: display })) : (_jsx("button", { type: "button", className: "kpt-mention", "data-kind": resolvedKind, children: display }));
28
+ return (_jsx(KptPeek, { className: cn('kpt-mention-host', className), trigger: trigger, placement: placement, openDelay: openDelay, label: display, ...rest, children: children }));
29
+ }
@@ -0,0 +1,31 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from 'react';
2
+ import { type KptMentionItem } from '@konce-pt/mention';
3
+ import type { KptPeekPlacement } from '../peek/peek.tsx';
4
+ export interface KptMentionsProps<T = unknown> extends Omit<ComponentPropsWithRef<'span'>, 'children' | 'prefix'> {
5
+ /** Treść z wzmiankami: `@id` albo `@[Nazwa ze spacją](id)`. */
6
+ text?: string;
7
+ /** Pozycje, po których rozwiązują się identyfikatory. Nieznane `id` zostaje etykietą. */
8
+ items?: readonly KptMentionItem<T>[];
9
+ /** Znak przed nazwą we wzmiance. */
10
+ prefix?: string;
11
+ placement?: KptPeekPlacement;
12
+ /** Karta wzmianki. Bez niej wzmianki są samym napisem, bo nie ma czego pokazać. */
13
+ renderCard?: (item: KptMentionItem<T> | null, id: string, label: string) => ReactNode;
14
+ }
15
+ /**
16
+ * Akapit ze wzmiankami — tekst wchodzi jako zwykły napis, a `@wzmianki` wychodzą z niego jako
17
+ * elementy z kartą podglądu. Rozbiciem zajmuje się `parseMentionText()` z `@konce-pt/mention`
18
+ * (czysty TS z testami), więc oba porty czytają tę samą treść tak samo.
19
+ *
20
+ * Wzmianek nie renderujemy z HTML-a: treść zostaje tekstem, a wzmianka jest elementem, który
21
+ * biblioteka sama zbudowała. Aplikacja nie musi więc przepuszczać przez sanitizer czegoś,
22
+ * co i tak pochodzi od jej użytkowników.
23
+ *
24
+ * @example
25
+ * <KptMentions
26
+ * text={note}
27
+ * items={people}
28
+ * renderCard={(item, id, label) => <KptCard>{item?.data?.role ?? label}</KptCard>}
29
+ * />
30
+ */
31
+ export declare function KptMentions<T = unknown>({ text, items, prefix, placement, renderCard, className, ...rest }: Readonly<KptMentionsProps<T>>): import("react").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { parseMentionText } from '@konce-pt/mention';
3
+ import { cn } from "../internal/cn.js";
4
+ import { KptMention } from "./mention.js";
5
+ /**
6
+ * Akapit ze wzmiankami — tekst wchodzi jako zwykły napis, a `@wzmianki` wychodzą z niego jako
7
+ * elementy z kartą podglądu. Rozbiciem zajmuje się `parseMentionText()` z `@konce-pt/mention`
8
+ * (czysty TS z testami), więc oba porty czytają tę samą treść tak samo.
9
+ *
10
+ * Wzmianek nie renderujemy z HTML-a: treść zostaje tekstem, a wzmianka jest elementem, który
11
+ * biblioteka sama zbudowała. Aplikacja nie musi więc przepuszczać przez sanitizer czegoś,
12
+ * co i tak pochodzi od jej użytkowników.
13
+ *
14
+ * @example
15
+ * <KptMentions
16
+ * text={note}
17
+ * items={people}
18
+ * renderCard={(item, id, label) => <KptCard>{item?.data?.role ?? label}</KptCard>}
19
+ * />
20
+ */
21
+ export function KptMentions({ text = '', items = [], prefix = '@', placement = 'bottom', renderCard, className, ...rest }) {
22
+ const segments = parseMentionText(text, items);
23
+ return (_jsx("span", { className: cn('kpt-mentions-host', className), ...rest, children: segments.map((segment, index) => segment.kind === 'text' ? (_jsx("span", { className: "kpt-mentions__text", children: segment.text }, index)) : (_jsx(KptMention, { id: segment.id, label: segment.label, kind: segment.item?.kind ?? '', prefix: prefix, placement: placement, children: renderCard?.(segment.item, segment.id, segment.label) }, index))) }));
24
+ }
@@ -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;