@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,53 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import type { KptChatAttachment, KptChatAuthor, KptChatGroupPosition, KptChatMessage as KptChatMessageModel, KptChatReaction, KptChatReactionOption, KptChatReplyRef, KptChatStatus } from '@konce-pt/chat';
3
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
4
+ /** Wygląd bąbla. `system` nie rysuje bąbla w ogóle — to komunikat, nie wypowiedź. */
5
+ export type KptChatMessageVariant = 'neutral' | 'primary' | 'solid' | 'ghost' | 'system';
6
+ export interface KptChatMessageProps extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
7
+ /** Cała wiadomość naraz. Pojedyncze propsy poniżej mają nad nią priorytet. */
8
+ message?: KptChatMessageModel | null;
9
+ author?: KptChatAuthor | null;
10
+ text?: string | null;
11
+ time?: Date | string | number | null;
12
+ attachments?: readonly KptChatAttachment[] | null;
13
+ reactions?: readonly KptChatReaction[] | null;
14
+ replyTo?: KptChatReplyRef | null;
15
+ status?: KptChatStatus | null;
16
+ /** Strona listy: `start` to cudza wiadomość, `end` własna. */
17
+ align?: 'start' | 'end';
18
+ /** Wariant kolorystyczny; bez niego wynika z `align` (własna = `primary`). */
19
+ variant?: KptChatMessageVariant | null;
20
+ position?: KptChatGroupPosition;
21
+ system?: boolean;
22
+ showAuthor?: boolean;
23
+ showAvatar?: boolean;
24
+ showMeta?: boolean;
25
+ tail?: boolean;
26
+ /** Akcje pod kursorem: odpowiedz i zareaguj. Domyślnie wyłączone. */
27
+ actions?: boolean;
28
+ /** Zestaw wybieraka reakcji. Domyślnie pięć wbudowanych; można go skrócić lub przestawić. */
29
+ reactionOptions?: readonly KptChatReactionOption[];
30
+ onRetry?: () => void;
31
+ onReply?: () => void;
32
+ /** Wybrana reakcja (emoji). Biblioteka niczego nie dopisuje do modelu — robi to aplikacja. */
33
+ onReact?: (emoji: string) => void;
34
+ /** Kliknięcie w cytat — aplikacja może przewinąć listę do oryginału. */
35
+ onQuoted?: (quote: KptChatReplyRef) => void;
36
+ /** Treść bąbla. Wygrywa z `text`, więc bąbel przyjmuje cokolwiek. */
37
+ children?: ReactNode;
38
+ }
39
+ /**
40
+ * Pojedyncza wiadomość czatu — bąbel z treścią, godziną, statusem wysyłki i opcjonalnym
41
+ * awatarem, cytatem, załącznikami i reakcjami.
42
+ *
43
+ * Działa na dwa sposoby: dostaje gotowy obiekt przez `message` (tak używa go `KptChat`)
44
+ * albo pojedyncze propsy, gdy budujesz listę samodzielnie.
45
+ *
46
+ * Wygląd wewnątrz grupy steruje `position`: zapadnięty narożnik od strony nadawcy i ogonek
47
+ * tylko przy ostatnim bąblu. Wylicza to `buildChatRows()` z `@konce-pt/chat`.
48
+ *
49
+ * @example
50
+ * <KptChatMessage message={msg} align="end" position="last" />
51
+ * <KptChatMessage align="end" status="failed" onRetry={resend}>Nie poszło</KptChatMessage>
52
+ */
53
+ export declare function KptChatMessage({ message, author, text, time, attachments, reactions, replyTo, status, align, variant, position, system, showAuthor, showAvatar, showMeta, tail, actions, reactionOptions, onRetry, onReply, onReact, onQuoted, children, locale, dictionary, className, ...rest }: Readonly<KptChatMessageProps>): import("react").JSX.Element;
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { KPT_CHAT_REACTIONS, canRetry, toDate } from '@konce-pt/chat';
4
+ import { cn } from "../internal/cn.js";
5
+ import { useKptMessages } from "../i18n/i18n-context.js";
6
+ import { KptAvatar } from "../avatar/avatar.js";
7
+ import { KptIcon } from "../icon/icon.js";
8
+ import { KptIconButton } from "../icon-button/icon-button.js";
9
+ /** Ikona statusu wysyłki. `read` używa tej samej ikony co `delivered`, różni je kolor. */
10
+ const STATUS_ICONS = {
11
+ sending: 'clock',
12
+ sent: 'check',
13
+ delivered: 'checks',
14
+ read: 'checks',
15
+ failed: 'alert-triangle',
16
+ };
17
+ /**
18
+ * Pojedyncza wiadomość czatu — bąbel z treścią, godziną, statusem wysyłki i opcjonalnym
19
+ * awatarem, cytatem, załącznikami i reakcjami.
20
+ *
21
+ * Działa na dwa sposoby: dostaje gotowy obiekt przez `message` (tak używa go `KptChat`)
22
+ * albo pojedyncze propsy, gdy budujesz listę samodzielnie.
23
+ *
24
+ * Wygląd wewnątrz grupy steruje `position`: zapadnięty narożnik od strony nadawcy i ogonek
25
+ * tylko przy ostatnim bąblu. Wylicza to `buildChatRows()` z `@konce-pt/chat`.
26
+ *
27
+ * @example
28
+ * <KptChatMessage message={msg} align="end" position="last" />
29
+ * <KptChatMessage align="end" status="failed" onRetry={resend}>Nie poszło</KptChatMessage>
30
+ */
31
+ export function KptChatMessage({ message = null, author = null, text = null, time = null, attachments = null, reactions = null, replyTo = null, status = null, align = 'start', variant = null, position = 'single', system = false, showAuthor = false, showAvatar = false, showMeta = true, tail = true, actions = false, reactionOptions = KPT_CHAT_REACTIONS, onRetry, onReply, onReact, onQuoted, children, locale, dictionary, className, ...rest }) {
32
+ const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
33
+ const rowRef = useRef(null);
34
+ /** Czy wybierak reakcji jest otwarty. */
35
+ const [pickerOpen, setPickerOpen] = useState(false);
36
+ // Kliknięcie obok zamyka wybierak. Sam `onBlur` nie wystarcza: w Safari kliknięcie w przycisk
37
+ // nie przenosi na niego fokusu, więc wybierak zostawałby otwarty na zawsze. Faza przechwytywania
38
+ // i `pointerdown` (nie `click`) — własne `onClick` opcji zdąży się wykonać przed zamknięciem.
39
+ useEffect(() => {
40
+ if (!pickerOpen)
41
+ return;
42
+ const close = (event) => {
43
+ if (!rowRef.current?.contains(event.target))
44
+ setPickerOpen(false);
45
+ };
46
+ document.addEventListener('pointerdown', close, true);
47
+ return () => document.removeEventListener('pointerdown', close, true);
48
+ }, [pickerOpen]);
49
+ const isSystem = system || (message?.system ?? false);
50
+ const resolvedAuthor = author ?? message?.author ?? null;
51
+ const resolvedText = text ?? message?.text ?? '';
52
+ const resolvedStatus = status ?? message?.status ?? null;
53
+ const resolvedReplyTo = replyTo ?? message?.replyTo ?? null;
54
+ const resolvedAttachments = attachments ?? message?.attachments ?? [];
55
+ const resolvedReactions = reactions ?? message?.reactions ?? [];
56
+ const rawTime = time ?? message?.sentAt ?? null;
57
+ const sentAt = rawTime === null ? null : toDate(rawTime);
58
+ const timeLabel = sentAt
59
+ ? new Intl.DateTimeFormat(activeLocale, { hour: '2-digit', minute: '2-digit' }).format(sentAt)
60
+ : '';
61
+ /*
62
+ * Wariant domyślny bierze się z wyrównania. Własna wiadomość dostaje pełny `solid`,
63
+ * bo motyw biblioteki jest monochromatyczny: pastelowy `primary-subtle` ma tę samą
64
+ * jasność co `muted`, więc własny i cudzy bąbel byłyby nie do odróżnienia. Kto woli
65
+ * ciszej, ustawia `variant="primary"` ręcznie.
66
+ */
67
+ const resolvedVariant = isSystem
68
+ ? 'system'
69
+ : (variant ?? (align === 'end' ? 'solid' : 'neutral'));
70
+ /* Ogonek tylko na końcu grupy i tylko tam, gdzie bąbel ma tło, z którego da się go wyciąć. */
71
+ const hasTail = tail &&
72
+ resolvedVariant !== 'ghost' &&
73
+ resolvedVariant !== 'system' &&
74
+ (position === 'last' || position === 'single');
75
+ const statusIcon = resolvedStatus ? STATUS_ICONS[resolvedStatus] : null;
76
+ const retryable = canRetry(resolvedStatus);
77
+ /*
78
+ * Etykieta dla czytnika ekranu. Bez niej z bąbla płynie sama treść — bez nadawcy,
79
+ * godziny i informacji, że wiadomość się nie wysłała.
80
+ */
81
+ const ariaLabel = isSystem
82
+ ? resolvedText
83
+ : [resolvedAuthor?.name, timeLabel, resolvedStatus ? t('chat.' + resolvedStatus) : '']
84
+ .filter(Boolean)
85
+ .join(', ');
86
+ if (isSystem) {
87
+ return (_jsx("div", { className: cn('kpt-chat-message-host', className), role: "note", "aria-label": ariaLabel, ...rest, children: _jsx("p", { className: "kpt-chat-message__system", children: resolvedText }) }));
88
+ }
89
+ return (_jsx("div", { className: cn('kpt-chat-message-host', className), role: "article", "aria-label": ariaLabel, ...rest, children: _jsxs("div", { ref: rowRef, className: "kpt-chat-message__row", "data-align": align, children: [align === 'start' ? (_jsx("span", { className: "kpt-chat-message__avatar", "aria-hidden": "true", children: showAvatar ? (_jsx(KptAvatar, { size: "sm", src: resolvedAuthor?.avatar ?? null, name: resolvedAuthor?.name ?? '' })) : null })) : null, _jsxs("div", { className: "kpt-chat-message__stack", children: [showAuthor && resolvedAuthor ? (_jsx("span", { className: "kpt-chat-message__author", children: resolvedAuthor.name })) : null, _jsxs("div", { className: "kpt-chat-message__bubble", "data-variant": resolvedVariant, "data-align": align, "data-position": position, "data-status": resolvedStatus ?? undefined, "data-tail": hasTail ? '' : undefined, children: [resolvedReplyTo ? (_jsxs("button", { type: "button", className: "kpt-chat-message__quote", onClick: () => onQuoted?.(resolvedReplyTo), children: [_jsx("span", { className: "kpt-chat-message__quote-author", children: resolvedReplyTo.author }), _jsx("span", { className: "kpt-chat-message__quote-text", children: resolvedReplyTo.excerpt })] })) : null, resolvedText ? _jsx("p", { className: "kpt-chat-message__text", children: resolvedText }) : null, children, resolvedAttachments.length ? (_jsx("ul", { className: "kpt-chat-message__files", children: resolvedAttachments.map((file) => (_jsxs("li", { className: "kpt-chat-message__file", children: [file.thumbnail ? (_jsx("img", { className: "kpt-chat-message__file-thumb", src: file.thumbnail, alt: file.name })) : (_jsx(KptIcon, { name: "paperclip" })), _jsx("a", { className: "kpt-chat-message__file-name", href: file.url, download: file.url ? file.name : undefined, children: file.name }), file.size ? (_jsx("span", { className: "kpt-chat-message__file-size", children: fileSize(file.size) })) : null] }, file.id))) })) : null, showMeta ? (_jsxs("span", { className: "kpt-chat-message__meta", children: [timeLabel ? (_jsx("time", { className: "kpt-chat-message__time", dateTime: sentAt?.toISOString(), children: timeLabel })) : null, statusIcon ? (_jsx(KptIcon, { className: "kpt-chat-message__status", name: statusIcon })) : null] })) : null, resolvedReactions.length ? (_jsx("ul", { className: "kpt-chat-message__reactions", "data-align": align, children: resolvedReactions.map((reaction) => (_jsxs("li", { className: "kpt-chat-message__reaction", "data-reacted": reaction.reacted ? '' : undefined, children: [_jsx("span", { "aria-hidden": "true", children: reaction.emoji }), _jsx("span", { className: "kpt-chat-message__reaction-count", children: reaction.count })] }, reaction.emoji))) })) : null] })] }), _jsx("span", { className: "kpt-chat-message__actions", "data-open": pickerOpen ? '' : undefined, children: retryable ? (_jsx(KptIconButton, { hostProps: { className: 'kpt-chat-message__retry' }, size: "sm", "aria-label": t('chat.retry'), onClick: () => onRetry?.(), children: _jsx(KptIcon, { name: "refresh" }) })) : actions ? (_jsxs(_Fragment, { children: [_jsx(KptIconButton, { size: "sm", "aria-label": t('chat.replyTo', { author: resolvedAuthor?.name ?? '' }), onClick: () => onReply?.(), children: _jsx(KptIcon, { name: "corner-up-left" }) }), _jsxs("span", { className: "kpt-chat-message__react", onKeyDown: (event) => {
90
+ if (event.key === 'Escape')
91
+ setPickerOpen(false);
92
+ }, onBlur: (event) => {
93
+ // Przechodzenie Tabem między opcjami wybieraka go nie zamyka.
94
+ if (!event.currentTarget.contains(event.relatedTarget))
95
+ setPickerOpen(false);
96
+ }, children: [_jsx(KptIconButton, { size: "sm", "aria-label": t('chat.react'), "aria-expanded": pickerOpen, onClick: () => setPickerOpen((open) => !open), children: _jsx(KptIcon, { name: "mood-smile" }) }), pickerOpen ? (_jsx("span", { className: "kpt-chat-message__picker", role: "group", "aria-label": t('chat.react'), children: reactionOptions.map((option) => {
97
+ const label = t('chat.react' + option.kind[0].toUpperCase() + option.kind.slice(1));
98
+ const reacted = resolvedReactions.some((item) => item.emoji === option.emoji && item.reacted);
99
+ return (_jsx("button", { type: "button", className: "kpt-chat-message__pick", "aria-label": label, title: label, "aria-pressed": reacted, onClick: () => {
100
+ onReact?.(option.emoji);
101
+ setPickerOpen(false);
102
+ }, children: _jsx("span", { "aria-hidden": "true", children: option.emoji }) }, option.kind));
103
+ }) })) : null] })] })) : null })] }) }));
104
+ }
105
+ /** Rozmiar załącznika w kB/MB — surowe bajty nic nie mówią przy pliku obok nazwy. */
106
+ function fileSize(bytes) {
107
+ if (bytes < 1024)
108
+ return bytes + ' B';
109
+ if (bytes < 1024 * 1024)
110
+ return Math.round(bytes / 1024) + ' kB';
111
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
112
+ }
@@ -0,0 +1,73 @@
1
+ # KptChatMessage (@konce-pt/react/chat)
2
+
3
+ A single chat bubble: content, a clock, a send status, and optionally an avatar, a quote,
4
+ attachments and reactions.
5
+ Import: `import { KptChatMessage } from '@konce-pt/react/chat';`
6
+
7
+ ## DOM
8
+ <div class="kpt-chat-message-host" role="article" aria-label="Anna, 09:22, Read">
9
+ <div class="kpt-chat-message__row" data-align="start">
10
+ <span class="kpt-chat-message__avatar">…KptAvatar…</span>
11
+ <div class="kpt-chat-message__stack">
12
+ <span class="kpt-chat-message__author">Anna Kowalska</span>
13
+ <div class="kpt-chat-message__bubble"
14
+ data-variant="neutral" data-align="start" data-position="last" data-tail>
15
+ <button class="kpt-chat-message__quote">…</button>
16
+ <p class="kpt-chat-message__text">…</p>
17
+ <ul class="kpt-chat-message__files">…</ul>
18
+ <span class="kpt-chat-message__meta"><time>09:22</time>…</span>
19
+ <ul class="kpt-chat-message__reactions">…</ul>
20
+ </div>
21
+ </div>
22
+ <span class="kpt-chat-message__actions">…KptIconButton, .kpt-chat-message__picker…</span>
23
+ </div>
24
+ </div>
25
+
26
+ ## Props
27
+ - `message`: `KptChatMessage` — the whole object; the single props below win over it
28
+ - `author`, `text`, `time`, `status`, `attachments`, `reactions`, `replyTo`
29
+ - `align`: `'start' | 'end'`; `variant`: `'neutral' | 'primary' | 'solid' | 'ghost' | 'system'`
30
+ - `position`: `'single' | 'first' | 'middle' | 'last'`; `system`: boolean
31
+ - `showAuthor`, `showAvatar`, `showMeta` (default true), `tail` (default true), `actions`
32
+ - `reactionOptions`: `readonly KptChatReactionOption[]` (default `KPT_CHAT_REACTIONS`) — the picker set
33
+ - `onRetry`, `onReply`: `() => void`; `onReact`: `(emoji: string) => void`; `onQuoted`: `(quote) => void`
34
+ - `children` — bubble content; it renders next to `text`, so the bubble takes anything
35
+ - `className`, `ref` and every other `<div>` prop go to the host
36
+
37
+ ## The default variant follows the alignment
38
+ Your own message gets the full `solid`, someone else's gets `neutral`. The theme is monochrome, so
39
+ the difference between bubbles is lightness, not hue — `--kpt-color-primary-subtle` has exactly the
40
+ lightness of `--kpt-color-muted`, which would make the two sides indistinguishable. For a quieter
41
+ list set `variant="primary"` by hand.
42
+
43
+ ## Grouping is what the shape is made of
44
+ `position` decides which corner collapses and which bubble wears the tail — inside a group the
45
+ corner on the sender's side drops to `--kpt-radius-sm`. `KptChat` gets all of it from
46
+ `buildChatRows()`; set it by hand only when you build the list yourself. The tail is skipped for
47
+ `ghost` and `system`, which have no background to cut it out of.
48
+
49
+ ## An unsent message must be able to say so
50
+ `status="failed"` outlines the bubble and puts a retry arrow **outside** it, in the action column
51
+ on the outer side of the list. `KptChat` also keeps the meta row visible on such a bubble even in
52
+ the middle of a group — the arrow with nothing to explain it would be a mystery.
53
+ Statuses: `sending` (clock), `sent` (check), `delivered` (double check), `read` (the same icon in
54
+ the accent colour) and `failed`.
55
+
56
+ ## Examples
57
+ <KptChatMessage message={msg} align="end" position="last" />
58
+ <KptChatMessage align="end" status="failed" onRetry={resend}>It did not go out</KptChatMessage>
59
+ <KptChatMessage system text="Anna joined the conversation" />
60
+
61
+ ## i18n
62
+ `retry`, `react`, `replyTo` (`{author}`) and the status names from the `chat` namespace. The clock
63
+ is formatted by `Intl` in the active locale.
64
+
65
+ ## Tokens
66
+ `--kpt-color-muted` (incoming) / `--kpt-color-primary` + `--kpt-color-on-primary` (own),
67
+ failed `--kpt-color-danger-subtle` + `--kpt-color-danger-border` — the failed rule also resets the
68
+ text colour, or a white `solid` label would land on pale pink.
69
+
70
+ ## Accessibility
71
+ The host is an `article` labelled with author, time and status — content alone would say nothing
72
+ about who wrote it or whether it was sent. The status icon is `aria-hidden`; the meaning travels in
73
+ the label. A system notice is a `note`, never an `article`.
@@ -0,0 +1,23 @@
1
+ import type { ComponentPropsWithRef } from 'react';
2
+ import type { KptChatAuthor } from '@konce-pt/chat';
3
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
4
+ export interface KptChatTypingProps extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
5
+ /** Osoby piszące w tej chwili. Pusta lista chowa wskaźnik. */
6
+ authors?: readonly KptChatAuthor[];
7
+ /** Własna etykieta zamiast tej ze słownika. */
8
+ label?: string;
9
+ }
10
+ /**
11
+ * Wskaźnik pisania — trzy kropki i informacja, kto pisze.
12
+ *
13
+ * Pusta lista autorów nie renderuje niczego, więc można go trzymać w drzewie na stałe
14
+ * i sterować wyłącznie danymi.
15
+ *
16
+ * Element jest `role="status"` z `aria-live="polite"`: czytnik ogłasza wejście i zniknięcie
17
+ * wskaźnika, ale nie przerywa czytania wiadomości. Sama animacja kropek nic nie mówi —
18
+ * informację niesie etykieta.
19
+ *
20
+ * @example
21
+ * <KptChatTyping authors={typing} />
22
+ */
23
+ export declare function KptChatTyping({ authors, label, locale, dictionary, className, ...rest }: Readonly<KptChatTypingProps>): import("react").JSX.Element | null;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../internal/cn.js";
3
+ import { useKptMessages } from "../i18n/i18n-context.js";
4
+ /**
5
+ * Wskaźnik pisania — trzy kropki i informacja, kto pisze.
6
+ *
7
+ * Pusta lista autorów nie renderuje niczego, więc można go trzymać w drzewie na stałe
8
+ * i sterować wyłącznie danymi.
9
+ *
10
+ * Element jest `role="status"` z `aria-live="polite"`: czytnik ogłasza wejście i zniknięcie
11
+ * wskaźnika, ale nie przerywa czytania wiadomości. Sama animacja kropek nic nie mówi —
12
+ * informację niesie etykieta.
13
+ *
14
+ * @example
15
+ * <KptChatTyping authors={typing} />
16
+ */
17
+ export function KptChatTyping({ authors = [], label = '', locale, dictionary, className, ...rest }) {
18
+ const { t } = useKptMessages({ locale, dictionary });
19
+ if (authors.length === 0 && !label)
20
+ return null;
21
+ /*
22
+ * Jedna osoba jest wymieniona z imienia, przy większej liczbie liczy się głowy —
23
+ * lista nazwisk rozjeżdża wiersz i i tak nikt jej nie czyta.
24
+ */
25
+ const text = label ||
26
+ (authors.length === 1
27
+ ? t('chat.typing', { name: authors[0].name })
28
+ : t('chat.typingMany', { count: authors.length }));
29
+ return (_jsx("div", { className: cn('kpt-chat-typing-host', className), role: "status", "aria-live": "polite", ...rest, children: _jsxs("div", { className: "kpt-chat-typing", children: [_jsxs("span", { className: "kpt-chat-typing__bubble", "aria-hidden": "true", children: [_jsx("span", { className: "kpt-chat-typing__dot" }), _jsx("span", { className: "kpt-chat-typing__dot" }), _jsx("span", { className: "kpt-chat-typing__dot" })] }), text ? _jsx("span", { className: "kpt-chat-typing__label", children: text }) : null] }) }));
30
+ }
@@ -0,0 +1,41 @@
1
+ # KptChatTyping (@konce-pt/react/chat)
2
+
3
+ The typing indicator — three dots and a line saying who is typing.
4
+ Import: `import { KptChatTyping } from '@konce-pt/react/chat';`
5
+
6
+ ## DOM
7
+ <div class="kpt-chat-typing-host" role="status" aria-live="polite">
8
+ <div class="kpt-chat-typing">
9
+ <span class="kpt-chat-typing__bubble" aria-hidden="true">
10
+ <span class="kpt-chat-typing__dot"></span>
11
+ <span class="kpt-chat-typing__dot"></span>
12
+ <span class="kpt-chat-typing__dot"></span>
13
+ </span>
14
+ <span class="kpt-chat-typing__label">Anna is typing…</span>
15
+ </div>
16
+ </div>
17
+
18
+ ## Props
19
+ - `authors`: `KptChatAuthor[]` — an empty list renders nothing at all
20
+ - `label`: string — your own wording instead of the dictionary's
21
+ - `className`, `ref` and every other `<div>` prop go to the host
22
+
23
+ ## One name, then a headcount
24
+ One person is named; from two up the component counts heads. A list of names breaks the line and
25
+ nobody reads it anyway.
26
+
27
+ ## Examples
28
+ <KptChatTyping authors={[anna]} />
29
+ <KptChatTyping authors={typing} />
30
+ <KptChatTyping label="The assistant is thinking…" />
31
+
32
+ ## i18n
33
+ `typing` (`{name}`) and `typingMany` (`{count}`) from the `chat` namespace.
34
+
35
+ ## Tokens
36
+ The bubble `--kpt-color-surface-variant`, the dots `--kpt-color-text-muted`.
37
+
38
+ ## Accessibility
39
+ `role="status"` with `aria-live="polite"` announces the indicator without interrupting a message
40
+ being read. Under `prefers-reduced-motion` the dots stop bouncing and pulse instead — they are the
41
+ only carrier of "someone is typing", so switching them off entirely would remove the message.
@@ -2,8 +2,16 @@ import type { ComponentPropsWithRef } from 'react';
2
2
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
3
  import type { KptHostProps } from '../internal/types.ts';
4
4
  export type KptChipVariant = 'neutral' | 'primary' | 'success' | 'danger' | 'warning' | 'info';
5
+ /** Wielkość liter etykiety. */
6
+ export type KptChipTextTransform = 'none' | 'capitalize' | 'uppercase' | 'lowercase';
5
7
  export interface KptChipProps extends ComponentPropsWithRef<'span'>, KptI18nOverrides {
6
8
  variant?: KptChipVariant;
9
+ /**
10
+ * Wielkość liter etykiety. `capitalize` podnosi pierwszą literę każdego słowa — także po
11
+ * dywizie („Agent-Ready"), bo CSS nie wie, gdzie kończy się wyraz złożony; przy takich
12
+ * napisach pewniejszy jest zapis wprost w treści.
13
+ */
14
+ textTransform?: KptChipTextTransform;
7
15
  /** Dokłada przycisk usuwania. */
8
16
  removable?: boolean;
9
17
  disabled?: boolean;
@@ -18,5 +26,6 @@ export interface KptChipProps extends ComponentPropsWithRef<'span'>, KptI18nOver
18
26
  * @example
19
27
  * <KptChip>Angular</KptChip>
20
28
  * <KptChip variant="success" removable onRemove={remove}>UI</KptChip>
29
+ * <KptChip textTransform="capitalize">nowość · zestaw startowy</KptChip>
21
30
  */
22
- export declare function KptChip({ variant, removable, disabled, onRemove, locale, dictionary, hostProps, className, children, ...rest }: Readonly<KptChipProps>): import("react").JSX.Element;
31
+ export declare function KptChip({ variant, textTransform, removable, disabled, onRemove, locale, dictionary, hostProps, className, children, ...rest }: Readonly<KptChipProps>): import("react").JSX.Element;
package/dist/chip/chip.js CHANGED
@@ -8,9 +8,10 @@ import { useKptMessages } from "../i18n/i18n-context.js";
8
8
  * @example
9
9
  * <KptChip>Angular</KptChip>
10
10
  * <KptChip variant="success" removable onRemove={remove}>UI</KptChip>
11
+ * <KptChip textTransform="capitalize">nowość · zestaw startowy</KptChip>
11
12
  */
12
- export function KptChip({ variant = 'neutral', removable = false, disabled = false, onRemove, locale, dictionary, hostProps, className, children, ...rest }) {
13
+ export function KptChip({ variant = 'neutral', textTransform = 'none', removable = false, disabled = false, onRemove, locale, dictionary, hostProps, className, children, ...rest }) {
13
14
  const { t } = useKptMessages({ locale, dictionary });
14
15
  const { className: hostClassName, ...hostRest } = hostProps ?? {};
15
- return (_jsx("span", { className: cn('kpt-chip-host', hostClassName), ...hostRest, children: _jsxs("span", { className: cn('kpt-chip', disabled && 'kpt-chip--disabled', className), "data-variant": variant, ...rest, children: [children, removable ? (_jsx("button", { type: "button", className: "kpt-chip__remove", disabled: disabled, onClick: onRemove, "aria-label": t('common.remove'), children: _jsx(KptIcon, { name: "x" }) })) : null] }) }));
16
+ return (_jsx("span", { className: cn('kpt-chip-host', hostClassName), ...hostRest, children: _jsxs("span", { className: cn('kpt-chip', disabled && 'kpt-chip--disabled', className), "data-variant": variant, "data-text-transform": textTransform === 'none' ? undefined : textTransform, ...rest, children: [children, removable ? (_jsx("button", { type: "button", className: "kpt-chip__remove", disabled: disabled, onClick: onRemove, "aria-label": t('common.remove'), children: _jsx(KptIcon, { name: "x" }) })) : null] }) }));
16
17
  }
@@ -13,6 +13,7 @@ Import: `import { KptChip } from '@konce-pt/react';`
13
13
 
14
14
  ## Props
15
15
  - `variant`: 'neutral' | 'primary' | 'success' | 'danger' | 'warning' | 'info' (default 'neutral')
16
+ - `textTransform`: 'none' | 'capitalize' | 'uppercase' | 'lowercase' (default 'none') — the label's letter case
16
17
  - `removable`: boolean — adds the remove button
17
18
  - `disabled`: boolean
18
19
  - `onRemove()` — fired by the remove button
@@ -31,6 +32,14 @@ The remove button's label comes from `common.remove` in the dictionary.
31
32
  <KptChip>React</KptChip>
32
33
  <KptChip variant="success" removable onRemove={() => drop(tag)}>UI</KptChip>
33
34
  <KptCard.Tags><KptChip variant="info">Daily</KptChip></KptCard.Tags>
35
+ <KptChip textTransform="capitalize">new · starter kit</KptChip>
36
+
37
+ ## Letter case
38
+ `textTransform` lands as `data-text-transform` on `.kpt-chip` and sets `text-transform` there, so
39
+ the label can stay in the application's own wording while the chip decides how it is displayed.
40
+ `capitalize` raises the first letter of every word — **including after a hyphen**
41
+ ("Agent-Ready"), because CSS does not know where a compound word ends. For labels with a hyphen,
42
+ spelling it out in the content is the exact route.
34
43
 
35
44
  ## Tokens
36
45
  `--kpt-color-muted`, `--kpt-color-on-muted` and the semantic colour pairs of each variant
@@ -6,8 +6,20 @@ export interface KptDataViewProps<T> extends Omit<ComponentPropsWithRef<'div'>,
6
6
  layout?: KptDataViewLayout;
7
7
  defaultLayout?: KptDataViewLayout;
8
8
  onLayoutChange?: (layout: KptDataViewLayout) => void;
9
- /** Rozmiar strony; 0 wyłącza stronicowanie. */
9
+ /** Rozmiar strony; 0 wyłącza stronicowanie. Sterowalny — select paginatora go zmienia. */
10
10
  pageSize?: number;
11
+ defaultPageSize?: number;
12
+ onPageSizeChange?: (pageSize: number) => void;
13
+ /** Rozmiary do wyboru w paginatorze. Pusta lista = select z jedną, bieżącą wartością. */
14
+ pageSizeOptions?: readonly number[];
15
+ /** Liczba kolumn siatki jako SUFIT, nie sztywna liczba; 0 = tyle, ile się zmieści. */
16
+ gridColumns?: number;
17
+ defaultGridColumns?: number;
18
+ onGridColumnsChange?: (columns: number) => void;
19
+ /** Wartości w pasku gęstości. Mniej niż dwie = paska nie ma po co pokazywać. */
20
+ gridColumnsOptions?: readonly number[];
21
+ /** Najwęższa sensowna karta — poniżej tego siatka zdejmuje kolumnę, zamiast ściskać. */
22
+ gridMinWidth?: string;
11
23
  paginated?: boolean;
12
24
  /** Szablon elementu — odpowiednik `<ng-template let-item>` z Angulara. */
13
25
  renderItem: (item: T, index: number) => ReactNode;
@@ -16,6 +28,6 @@ export interface KptDataViewProps<T> extends Omit<ComponentPropsWithRef<'div'>,
16
28
  * DataView — lista elementów w układzie listy albo siatki, z opcjonalnym stronicowaniem.
17
29
  *
18
30
  * @example
19
- * <KptDataView items={products} pageSize={6} renderItem={(p) => <div>{p.name}</div>} />
31
+ * <KptDataView items={products} defaultPageSize={6} renderItem={(p) => <div>{p.name}</div>} />
20
32
  */
21
- export declare function KptDataView<T>({ items, layout, defaultLayout, onLayoutChange, pageSize, paginated, renderItem, locale, dictionary, className, ...rest }: Readonly<KptDataViewProps<T>>): import("react").JSX.Element;
33
+ export declare function KptDataView<T>({ items, layout, defaultLayout, onLayoutChange, pageSize, defaultPageSize, onPageSizeChange, pageSizeOptions, gridColumns, defaultGridColumns, onGridColumnsChange, gridColumnsOptions, gridMinWidth, paginated, renderItem, locale, dictionary, className, ...rest }: Readonly<KptDataViewProps<T>>): import("react").JSX.Element;
@@ -9,17 +9,34 @@ import { KptPaginator } from "../paginator/paginator.js";
9
9
  * DataView — lista elementów w układzie listy albo siatki, z opcjonalnym stronicowaniem.
10
10
  *
11
11
  * @example
12
- * <KptDataView items={products} pageSize={6} renderItem={(p) => <div>{p.name}</div>} />
12
+ * <KptDataView items={products} defaultPageSize={6} renderItem={(p) => <div>{p.name}</div>} />
13
13
  */
14
- export function KptDataView({ items = [], layout, defaultLayout = 'grid', onLayoutChange, pageSize = 0, paginated = true, renderItem, locale, dictionary, className, ...rest }) {
14
+ export function KptDataView({ items = [], layout, defaultLayout = 'grid', onLayoutChange, pageSize, defaultPageSize = 0, onPageSizeChange, pageSizeOptions = [], gridColumns, defaultGridColumns = 0, onGridColumnsChange, gridColumnsOptions = [], gridMinWidth = '10rem', paginated = true, renderItem, locale, dictionary, className, ...rest }) {
15
15
  const { t } = useKptMessages({ locale, dictionary });
16
16
  const [currentLayout, setLayout] = useControllableState({
17
17
  value: layout,
18
18
  defaultValue: defaultLayout,
19
19
  onChange: onLayoutChange,
20
20
  });
21
+ const [size, setSize] = useControllableState({
22
+ value: pageSize,
23
+ defaultValue: defaultPageSize,
24
+ onChange: onPageSizeChange,
25
+ });
26
+ const [columns, setColumns] = useControllableState({
27
+ value: gridColumns,
28
+ defaultValue: defaultGridColumns,
29
+ onChange: onGridColumnsChange,
30
+ });
21
31
  const [pageIndex, setPageIndex] = useState(0);
22
- const showPager = paginated && pageSize > 0 && items.length > pageSize;
23
- const pageItems = !paginated || pageSize <= 0 ? items : items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize);
24
- return (_jsx("div", { className: cn('kpt-data-view-host', className), ...rest, children: _jsxs("div", { className: "kpt-data-view", children: [_jsxs("div", { className: "kpt-data-view__toolbar", children: [_jsx("div", { className: "kpt-data-view__count", children: t('dataView.count', { count: items.length }) }), _jsxs("div", { className: "kpt-data-view__switch", children: [_jsx("button", { type: "button", className: cn(currentLayout === 'list' && 'is-active'), "aria-label": t('dataView.list'), onClick: () => setLayout('list'), children: _jsx(KptIcon, { name: "list" }) }), _jsx("button", { type: "button", className: cn(currentLayout === 'grid' && 'is-active'), "aria-label": t('dataView.grid'), onClick: () => setLayout('grid'), children: _jsx(KptIcon, { name: "menu" }) })] })] }), _jsx("div", { className: "kpt-data-view__content", "data-layout": currentLayout, children: pageItems.length ? (pageItems.map((item, index) => (_jsx("div", { className: "kpt-data-view__item", children: renderItem(item, index) }, index)))) : (_jsx("div", { className: "kpt-data-view__empty", children: t('dataView.empty') })) }), showPager ? (_jsx(KptPaginator, { length: items.length, pageIndex: pageIndex, onPageIndexChange: setPageIndex, pageSize: pageSize, pageSizeOptions: [pageSize] })) : null] }) }));
32
+ // Opcje selecta: własna lista albo — gdy pusta — sam bieżący rozmiar.
33
+ const pagerSizeOptions = pageSizeOptions.length ? pageSizeOptions : [size];
34
+ // Pager zostaje na ekranie także wtedy, gdy wszystko mieści się na jednej stronie, o ile jest
35
+ // w czym wybierać. Bez tego wybranie największego rozmiaru chowa select, którym użytkownik
36
+ // właśnie się posłużył — i nie ma jak wrócić do mniejszego.
37
+ // Pasek gęstości ma sens tylko w siatce i tylko wtedy, gdy jest w czym wybierać.
38
+ const showColumns = currentLayout === 'grid' && gridColumnsOptions.length > 1;
39
+ const showPager = paginated && size > 0 && (items.length > size || pagerSizeOptions.length > 1);
40
+ const pageItems = !paginated || size <= 0 ? items : items.slice(pageIndex * size, pageIndex * size + size);
41
+ return (_jsx("div", { className: cn('kpt-data-view-host', className), ...rest, children: _jsxs("div", { className: "kpt-data-view", children: [_jsxs("div", { className: "kpt-data-view__toolbar", children: [_jsx("div", { className: "kpt-data-view__count", children: t('dataView.count', { count: items.length }) }), _jsxs("div", { className: "kpt-data-view__controls", children: [showColumns ? (_jsx("div", { className: "kpt-data-view__columns", role: "group", "aria-label": t('dataView.columns'), children: gridColumnsOptions.map((n) => (_jsxs("button", { type: "button", className: cn(columns === n && 'is-active'), "aria-pressed": columns === n, onClick: () => setColumns(n), children: [_jsx(KptIcon, { name: "grid-dots" }), n] }, n))) })) : null, _jsxs("div", { className: "kpt-data-view__switch", children: [_jsx("button", { type: "button", className: cn(currentLayout === 'list' && 'is-active'), "aria-label": t('dataView.list'), "aria-pressed": currentLayout === 'list', onClick: () => setLayout('list'), children: _jsx(KptIcon, { name: "list" }) }), _jsx("button", { type: "button", className: cn(currentLayout === 'grid' && 'is-active'), "aria-label": t('dataView.grid'), "aria-pressed": currentLayout === 'grid', onClick: () => setLayout('grid'), children: _jsx(KptIcon, { name: "layout-grid" }) })] })] })] }), _jsx("div", { className: "kpt-data-view__content", "data-layout": currentLayout, "data-columns": columns ? '' : undefined, style: { '--_min': gridMinWidth, '--_cols': columns || undefined }, children: pageItems.length ? (pageItems.map((item, index) => (_jsx("div", { className: "kpt-data-view__item", children: renderItem(item, index) }, index)))) : (_jsx("div", { className: "kpt-data-view__empty", children: t('dataView.empty') })) }), showPager ? (_jsx(KptPaginator, { length: items.length, pageIndex: pageIndex, onPageIndexChange: setPageIndex, pageSize: size, onPageSizeChange: setSize, pageSizeOptions: pagerSizeOptions })) : null] }) }));
25
42
  }
@@ -20,7 +20,11 @@ Import: `import { KptDataView } from '@konce-pt/react';`
20
20
  ## Props
21
21
  - `items`: `T[]`
22
22
  - `layout` / `defaultLayout` / `onLayoutChange` — `'list' | 'grid'` (default `'grid'`)
23
- - `pageSize`: number — 0 turns pagination off
23
+ - `pageSize` / `defaultPageSize` / `onPageSizeChange` — number; 0 turns pagination off
24
+ - `pageSizeOptions`: number[] (empty = the select offers the current size only)
25
+ - `gridColumns` / `defaultGridColumns` / `onGridColumnsChange` — number; 0 = as many as fit
26
+ - `gridColumnsOptions`: number[] (fewer than two = no density bar)
27
+ - `gridMinWidth`: string (default `10rem`) — the narrowest card worth rendering
24
28
  - `paginated`: boolean (default true)
25
29
  - `renderItem(item, index)` — required; the equivalent of `<ng-template let-item>`
26
30
  - `className`, `ref` and every other `<div>` prop go to the host
@@ -29,20 +33,29 @@ Import: `import { KptDataView } from '@konce-pt/react';`
29
33
  `data-layout` on the content switches the grid rules; the toolbar only flips it. Everything else —
30
34
  the item's markup — is the caller's, which is why there are no card props here.
31
35
 
36
+ ## The column count is a ceiling
37
+ The density bar shows up in the grid layout when `gridColumnsOptions` holds more than one value.
38
+ The chosen number caps the grid rather than pinning it: the track minimum grows to the width of
39
+ one of N columns, so a narrow container drops to two or one column instead of squeezing cards
40
+ below `gridMinWidth`.
41
+
32
42
  ## The pager appears when it is needed
33
- Only when `paginated`, `pageSize > 0` and there are more items than fit on a page — a paginator
34
- under a single page of results is noise.
43
+ Only when `paginated`, the page size is above 0, and either the items outgrow one page or
44
+ `pageSizeOptions` holds more than one size — a paginator under a single page of results is noise,
45
+ but hiding it right after someone picks the largest size would strand them there.
35
46
 
36
47
  ## Examples
37
- <KptDataView items={products} pageSize={6} renderItem={(p) => <div>{p.name}</div>} />
48
+ <KptDataView items={products} defaultPageSize={6} renderItem={(p) => <div>{p.name}</div>} />
49
+ <KptDataView items={products} defaultPageSize={6} pageSizeOptions={[3, 6, 9]} renderItem={renderCard} />
50
+ <KptDataView items={products} defaultGridColumns={2} gridColumnsOptions={[2, 3, 4]} gridMinWidth="14rem" renderItem={renderCard} />
38
51
  <KptDataView items={products} layout="list" paginated={false} renderItem={renderCard} />
39
52
 
40
53
  ## i18n
41
- The count and the two switch buttons come from the `dataView` namespace (`count` with `{count}`,
42
- `list`, `grid`, `empty`).
54
+ The count, the two switch buttons and the density group come from the `dataView` namespace
55
+ (`count` with `{count}`, `list`, `grid`, `columns`, `empty`).
43
56
 
44
57
  ## Tokens
45
- The grid gap `--kpt-space-4`, the active switch `--kpt-color-primary`.
58
+ The grid gap `--kpt-space-3`, the active switch `--kpt-color-primary`.
46
59
 
47
60
  ## Accessibility
48
61
  Both layout buttons carry labels from the dictionary; an empty list renders a message instead of
@@ -0,0 +1 @@
1
+ export declare function isDevBuild(): boolean;
@@ -0,0 +1,3 @@
1
+ export function isDevBuild() {
2
+ return typeof process !== 'undefined' && process?.env?.['NODE_ENV'] !== 'production';
3
+ }
@@ -21,6 +21,11 @@ export interface KptOverlayProps {
21
21
  * zasłoniłby to pole i zabrałby mu każde kliknięcie.
22
22
  */
23
23
  withBackdrop?: boolean;
24
+ /**
25
+ * Otwieraj nad wyzwalaczem, dopóki jest tam miejsce (odpowiednik `placement="top"` w porcie
26
+ * Angulara, gdzie kolejnością pozycji steruje CDK). Bez tego panel wychodzi spod wyzwalacza.
27
+ */
28
+ preferAbove?: boolean;
24
29
  children: ReactNode;
25
30
  }
26
- export declare function KptOverlay({ open, anchor, onClose, matchAnchorWidth, offset, align, withBackdrop, children, }: Readonly<KptOverlayProps>): import("react").ReactPortal | null;
31
+ export declare function KptOverlay({ open, anchor, onClose, matchAnchorWidth, offset, align, withBackdrop, preferAbove, children, }: Readonly<KptOverlayProps>): import("react").ReactPortal | null;
@@ -4,7 +4,7 @@ import { createPortal } from 'react-dom';
4
4
  import { useKptOverlayContainer } from "./overlay-container.js";
5
5
  /** Ta sama warstwa co `--kpt-z-overlay` w tokenach — panel ma być nad treścią aplikacji. */
6
6
  const Z_INDEX = 'var(--kpt-z-overlay, 1100)';
7
- export function KptOverlay({ open, anchor, onClose, matchAnchorWidth = false, offset = 4, align = 'start', withBackdrop = true, children, }) {
7
+ export function KptOverlay({ open, anchor, onClose, matchAnchorWidth = false, offset = 4, align = 'start', withBackdrop = true, preferAbove = false, children, }) {
8
8
  const container = useKptOverlayContainer();
9
9
  const panelRef = useRef(null);
10
10
  const [position, setPosition] = useState(null);
@@ -17,13 +17,15 @@ export function KptOverlay({ open, anchor, onClose, matchAnchorWidth = false, of
17
17
  const below = window.innerHeight - rect.bottom;
18
18
  // Odbicie w górę tylko wtedy, gdy pod spodem naprawdę brakuje miejsca, a nad wyzwalaczem
19
19
  // jest go więcej — inaczej panel skakałby przy każdym przewinięciu o kilka pikseli.
20
- const flip = panelHeight > 0 && below < panelHeight + offset && rect.top > below;
20
+ // Przy `preferAbove` jest odwrotnie: panel stoi nad wyzwalaczem, dopóki się tam mieści.
21
+ const fitsAbove = panelHeight === 0 || rect.top > panelHeight + offset;
22
+ const flip = preferAbove ? fitsAbove : panelHeight > 0 && below < panelHeight + offset && rect.top > below;
21
23
  setPosition({
22
24
  top: flip ? rect.top - panelHeight - offset : rect.bottom + offset,
23
25
  left: align === 'end' ? rect.right - panelWidth : rect.left,
24
26
  ...(matchAnchorWidth ? { width: rect.width } : {}),
25
27
  });
26
- }, [anchor, matchAnchorWidth, offset, align]);
28
+ }, [anchor, matchAnchorWidth, offset, align, preferAbove]);
27
29
  useLayoutEffect(() => {
28
30
  if (!open) {
29
31
  setPosition(null);