@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.
- package/README.md +11 -4
- package/dist/backdrop/backdrop.d.ts +79 -0
- package/dist/backdrop/backdrop.js +195 -0
- package/dist/backdrop/llms.txt +132 -0
- package/dist/chat/chat.d.ts +53 -0
- package/dist/chat/chat.js +160 -0
- package/dist/chat/llms.txt +78 -0
- package/dist/chat-composer/chat-composer.d.ts +41 -0
- package/dist/chat-composer/chat-composer.js +81 -0
- package/dist/chat-composer/llms.txt +56 -0
- package/dist/chat-dock/chat-dock.d.ts +40 -0
- package/dist/chat-dock/chat-dock.js +67 -0
- package/dist/chat-dock/llms.txt +71 -0
- package/dist/chat-entry/public-api.d.ts +12 -0
- package/dist/chat-entry/public-api.js +10 -0
- package/dist/chat-message/chat-message.d.ts +53 -0
- package/dist/chat-message/chat-message.js +112 -0
- package/dist/chat-message/llms.txt +73 -0
- package/dist/chat-typing/chat-typing.d.ts +23 -0
- package/dist/chat-typing/chat-typing.js +30 -0
- package/dist/chat-typing/llms.txt +41 -0
- package/dist/chip/chip.d.ts +10 -1
- package/dist/chip/chip.js +3 -2
- package/dist/chip/llms.txt +9 -0
- package/dist/data-view/data-view.d.ts +15 -3
- package/dist/data-view/data-view.js +22 -5
- package/dist/data-view/llms.txt +20 -7
- package/dist/internal/dev.d.ts +1 -0
- package/dist/internal/dev.js +3 -0
- package/dist/internal/overlay.d.ts +6 -1
- package/dist/internal/overlay.js +5 -3
- package/dist/mention/llms.txt +60 -0
- package/dist/mention/mention.d.ts +38 -0
- package/dist/mention/mention.js +29 -0
- package/dist/mention/mentions.d.ts +31 -0
- package/dist/mention/mentions.js +24 -0
- package/dist/order-list/llms.txt +16 -5
- package/dist/order-list/order-list.d.ts +5 -3
- package/dist/order-list/order-list.js +69 -4
- package/dist/peek/llms.txt +42 -0
- package/dist/peek/peek.d.ts +35 -0
- package/dist/peek/peek.js +127 -0
- package/dist/pick-list/llms.txt +19 -5
- package/dist/pick-list/pick-list.d.ts +7 -2
- package/dist/pick-list/pick-list.js +123 -17
- package/dist/public-api.d.ts +13 -1
- package/dist/public-api.js +6 -0
- package/dist/rich-text/llms.txt +10 -0
- package/dist/rich-text/rich-text.d.ts +11 -1
- package/dist/rich-text/rich-text.js +157 -5
- package/dist/rich-text/sanitize.js +10 -1
- package/dist/roadmap/roadmap.js +1 -3
- 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.
|
package/dist/chip/chip.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/chip/llms.txt
CHANGED
|
@@ -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}
|
|
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}
|
|
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
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
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
|
}
|
package/dist/data-view/llms.txt
CHANGED
|
@@ -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
|
|
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`,
|
|
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}
|
|
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
|
|
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-
|
|
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;
|
|
@@ -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;
|
package/dist/internal/overlay.js
CHANGED
|
@@ -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
|
-
|
|
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);
|