@konce-pt/react 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -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
package/README.md
CHANGED
|
@@ -169,9 +169,9 @@ Data table with sorting, filtering, pagination and a custom cell:
|
|
|
169
169
|
| **Navigation** | `KptTabs`, `KptAccordion`, `KptBreadcrumb`, `KptStepper`, `KptMenu`, `KptMenubar`, `KptMegamenu`, `KptContextMenu` |
|
|
170
170
|
| **Data** | `KptDataTable`, `KptPaginator`, `KptTree`, `KptTimeline`, `KptCarousel`, `KptDataView`, `KptPickList`, `KptOrderList`, `KptGalleria`, `KptMeterGroup` |
|
|
171
171
|
| **Feedback & overlay** | `KptAlert`, `KptDialog`, `kptToast` + `KptToastContainer`, `KptTooltip`, `KptPopover`, `KptDrawer`, `KptBottomSheet`, `kptConfirm`, `KptBadge`, `KptChip`, `KptAvatar`, `KptAvatarGroup`, `KptSpinner`, `KptProgress`, `KptSkeleton`, `KptEmpty`, `KptImage`, `KptAuth` |
|
|
172
|
-
| **Subpaths** | `@konce-pt/react/charts` (`KptChart`), `/roadmap` (`KptRoadmap`), `/grid` (`KptGrid`, `KptCol`, `KptFlex`), `/map` (`KptMap` + legend, POI card, search), `/icons` (full Tabler set) |
|
|
172
|
+
| **Subpaths** | `@konce-pt/react/charts` (`KptChart`), `/chat` (`KptChat`, `KptChatMessage`, `KptChatTyping`, `KptChatComposer`, `KptChatDock`), `/roadmap` (`KptRoadmap`), `/grid` (`KptGrid`, `KptCol`, `KptFlex`), `/map` (`KptMap` + legend, POI card, search), `/icons` (full Tabler set) |
|
|
173
173
|
|
|
174
|
-
The heavy pieces (charts, roadmap, maps, the full icon set) live in subpaths on purpose, so the core
|
|
174
|
+
The heavy pieces (charts, chat, roadmap, maps, the full icon set) live in subpaths on purpose, so the core
|
|
175
175
|
entry point stays small. Every component ships an `llms.txt` API sheet next to its source.
|
|
176
176
|
|
|
177
177
|
## Theming
|
|
@@ -393,8 +393,8 @@ Angulara.
|
|
|
393
393
|
### Komponenty (80+)
|
|
394
394
|
|
|
395
395
|
Formularze, przyciski i akcje, layout, nawigacja, dane (z flagową `KptDataTable`), feedback
|
|
396
|
-
i overlay — pełna lista w tabeli powyżej. Ciężkie rzeczy (wykresy, roadmapa, mapy, pełny Tabler)
|
|
397
|
-
siedzą w subpathach `@konce-pt/react/{charts,roadmap,grid,map,icons}`, żeby nie obciążać rdzenia.
|
|
396
|
+
i overlay — pełna lista w tabeli powyżej. Ciężkie rzeczy (wykresy, czat, roadmapa, mapy, pełny Tabler)
|
|
397
|
+
siedzą w subpathach `@konce-pt/react/{charts,chat,roadmap,grid,map,icons}`, żeby nie obciążać rdzenia.
|
|
398
398
|
Każdy komponent ma obok źródła plik `llms.txt` z opisem API.
|
|
399
399
|
|
|
400
400
|
### Dokumentacja
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef } from 'react';
|
|
2
|
+
import { type KptBackdropAnchor, type KptBackdropEffect } from '@konce-pt/backdrop';
|
|
3
|
+
export type { KptBackdropAnchor, KptBackdropEffect } from '@konce-pt/backdrop';
|
|
4
|
+
export interface KptBackdropProps extends ComponentPropsWithRef<'div'> {
|
|
5
|
+
/** Rysowany efekt. */
|
|
6
|
+
effect?: KptBackdropEffect;
|
|
7
|
+
/** Tło sekcji. Puste bierze `--kpt-color-surface`. */
|
|
8
|
+
background?: string;
|
|
9
|
+
/** Tempo; 1 to spokojne tempo obu efektów, 0 zatrzymuje obraz. */
|
|
10
|
+
speed?: number;
|
|
11
|
+
/** Ziarno losowania — ta sama wartość daje ten sam układ kwadratów i ten sam przebieg wstęgi. */
|
|
12
|
+
seed?: number;
|
|
13
|
+
/** Zatrzymuje animację, zostawiając ostatnią klatkę. */
|
|
14
|
+
paused?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Przenosi barwy wpisane na sztywno na powierzchnię bieżącego motywu: czerń dobrana do białego
|
|
17
|
+
* tła staje się bielą, gdy tło jest ciemne. Rusza wyłącznie jasność — odcień i nasycenie
|
|
18
|
+
* zostają, więc barwa marki nie zmienia się w inną barwę.
|
|
19
|
+
*
|
|
20
|
+
* Dotyczy `color`, `colors` i `background`, ale tylko wtedy, gdy podano je literałem. Prop pusty
|
|
21
|
+
* bierze token, a zapis z `var()` sam po token sięga — jedno i drugie już idzie za motywem
|
|
22
|
+
* i adaptacja odwróciłaby je drugi raz.
|
|
23
|
+
*/
|
|
24
|
+
adaptive?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* `signal-grid`: kolor kwadratów — dowolny zapis CSS, także sięgający po tokeny, np.
|
|
27
|
+
* `color-mix(in oklab, #5b8af2, var(--kpt-color-surface) 30%)`. Taki zapis przelicza się przy
|
|
28
|
+
* zmianie motywu; literał zostaje literałem. Puste bierze `--kpt-color-primary`.
|
|
29
|
+
*/
|
|
30
|
+
color?: string;
|
|
31
|
+
/** `signal-grid`: przy której krawędzi siatka jest najgęstsza. */
|
|
32
|
+
anchor?: KptBackdropAnchor;
|
|
33
|
+
/** `signal-grid`: bok kwadratu (px). */
|
|
34
|
+
cell?: number;
|
|
35
|
+
/** `signal-grid`: odstęp między kwadratami (px). */
|
|
36
|
+
gap?: number;
|
|
37
|
+
/** `signal-grid`: udział zapalonych komórek przy kotwicy, 0–1. */
|
|
38
|
+
density?: number;
|
|
39
|
+
/** `signal-grid`: jak szybko siatka gaśnie ku przeciwnej krawędzi. */
|
|
40
|
+
falloff?: number;
|
|
41
|
+
/**
|
|
42
|
+
* `wave`: rampa barw, po której przesuwa się wstęga — każdy przystanek dowolnym zapisem CSS.
|
|
43
|
+
* Zamknięta w pętlę, więc ostatnia barwa wraca do pierwszej. Pusta bierze `--kpt-color-primary`
|
|
44
|
+
* i dwie barwy wyprowadzone z niego obrotem odcienia — paleta biblioteki jest neutralna, więc
|
|
45
|
+
* domyślna wstęga jest jednobarwna i przygaszona. Po kolory sięga się tu, nie do tokenów
|
|
46
|
+
* statusowych. Zapis może sięgać po tokeny i wtedy idzie za motywem. Przystanek, którego
|
|
47
|
+
* przeglądarka nie uznaje za kolor, wypada z rampy; gdy wypadną wszystkie, zostaje rampa
|
|
48
|
+
* domyślna.
|
|
49
|
+
*/
|
|
50
|
+
colors?: readonly string[];
|
|
51
|
+
/** `wave`: kąt, pod jakim prąd przecina kadr (stopnie); 0 to poziomo w prawo. */
|
|
52
|
+
rotation?: number;
|
|
53
|
+
/** `wave`: szerokość wstęgi jako ułamek wysokości kadru. */
|
|
54
|
+
band?: number;
|
|
55
|
+
/** `wave`: jak mocno faluje linia środkowa. Zero zatrzymuje falowanie, nie barwy. */
|
|
56
|
+
amplitude?: number;
|
|
57
|
+
/** Propsy warstwy z treścią (`.kpt-backdrop__content`). */
|
|
58
|
+
contentProps?: ComponentPropsWithRef<'div'>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Dekoracyjna warstwa tła dla sekcji: komponent obejmuje treść i maluje pod nią animowany canvas.
|
|
62
|
+
* Sięga się po niego tam, gdzie płaska powierzchnia wygląda na niedokończoną, a wideo albo duży
|
|
63
|
+
* obraz kosztowałyby transfer i nie poszłyby za motywem.
|
|
64
|
+
*
|
|
65
|
+
* Komponent jest opakowaniem: canvas leży pod spodem, a dzieci idą warstwę wyżej i zachowują się
|
|
66
|
+
* zwyczajnie. Rysowanie stoi, gdy nie ma czego oglądać — sekcja poza ekranem, karta w tle,
|
|
67
|
+
* `paused`, `speed` równe zeru albo ograniczony ruch w systemie; wtedy zostaje jedna statyczna
|
|
68
|
+
* klatka zamiast pustki, bo efekt jest częścią kompozycji, nie ozdobą.
|
|
69
|
+
*
|
|
70
|
+
* Rysowanie i cała matematyka siedzą w `@konce-pt/backdrop`, wspólnym z portem Angulara —
|
|
71
|
+
* ten komponent to sam cykl życia.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* <KptBackdrop effect="wave" rotation={18} colors={['#5b8af2', '#a86df2', '#f25b9e']}>
|
|
75
|
+
* <h1>Build your app from the first prompt</h1>
|
|
76
|
+
* <KptButton>Get started</KptButton>
|
|
77
|
+
* </KptBackdrop>
|
|
78
|
+
*/
|
|
79
|
+
export declare function KptBackdrop({ effect, background, speed, seed, paused, adaptive, color, anchor, cell, gap, density, falloff, colors, rotation, band, amplitude, contentProps, className, style, children, ...rest }: Readonly<KptBackdropProps>): import("react").JSX.Element;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { KPT_BACKDROP_MAX_DPR, KPT_BACKDROP_MAX_STEP, KPT_SIGNAL_GRID_CELL, KPT_SIGNAL_GRID_DENSITY, KPT_SIGNAL_GRID_FALLOFF, KPT_SIGNAL_GRID_GAP, KPT_WAVE_AMPLITUDE, KPT_WAVE_BAND, KPT_WAVE_ROTATION, adaptBackdropValue, createBackdropRenderer, observeBackdropTheme, readBackdropInk, readBackdropLead, readBackdropSurface, resolveBackdropColors, } from '@konce-pt/backdrop';
|
|
4
|
+
import { cn } from "../internal/cn.js";
|
|
5
|
+
import { isDevBuild } from "../internal/dev.js";
|
|
6
|
+
const EMPTY = [];
|
|
7
|
+
/**
|
|
8
|
+
* Dekoracyjna warstwa tła dla sekcji: komponent obejmuje treść i maluje pod nią animowany canvas.
|
|
9
|
+
* Sięga się po niego tam, gdzie płaska powierzchnia wygląda na niedokończoną, a wideo albo duży
|
|
10
|
+
* obraz kosztowałyby transfer i nie poszłyby za motywem.
|
|
11
|
+
*
|
|
12
|
+
* Komponent jest opakowaniem: canvas leży pod spodem, a dzieci idą warstwę wyżej i zachowują się
|
|
13
|
+
* zwyczajnie. Rysowanie stoi, gdy nie ma czego oglądać — sekcja poza ekranem, karta w tle,
|
|
14
|
+
* `paused`, `speed` równe zeru albo ograniczony ruch w systemie; wtedy zostaje jedna statyczna
|
|
15
|
+
* klatka zamiast pustki, bo efekt jest częścią kompozycji, nie ozdobą.
|
|
16
|
+
*
|
|
17
|
+
* Rysowanie i cała matematyka siedzą w `@konce-pt/backdrop`, wspólnym z portem Angulara —
|
|
18
|
+
* ten komponent to sam cykl życia.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <KptBackdrop effect="wave" rotation={18} colors={['#5b8af2', '#a86df2', '#f25b9e']}>
|
|
22
|
+
* <h1>Build your app from the first prompt</h1>
|
|
23
|
+
* <KptButton>Get started</KptButton>
|
|
24
|
+
* </KptBackdrop>
|
|
25
|
+
*/
|
|
26
|
+
export function KptBackdrop({ effect = 'signal-grid', background = '', speed = 1, seed = 0, paused = false, adaptive = false, color = '', anchor = 'right', cell = KPT_SIGNAL_GRID_CELL, gap = KPT_SIGNAL_GRID_GAP, density = KPT_SIGNAL_GRID_DENSITY, falloff = KPT_SIGNAL_GRID_FALLOFF, colors = EMPTY, rotation = KPT_WAVE_ROTATION, band = KPT_WAVE_BAND, amplitude = KPT_WAVE_AMPLITUDE, contentProps, className, style, children, ...rest }) {
|
|
27
|
+
const host = useRef(null);
|
|
28
|
+
const canvas = useRef(null);
|
|
29
|
+
const context = useRef(null);
|
|
30
|
+
const renderer = useRef(null);
|
|
31
|
+
const size = useRef({ width: 0, height: 0 });
|
|
32
|
+
/** Zegar animacji w sekundach — w refie, żeby przebudowa renderera nie cofała obrazu. */
|
|
33
|
+
const clock = useRef(0);
|
|
34
|
+
/** Kolor wiodący, z którym zbudowany jest bieżący renderer — punkt odniesienia dla zmiany motywu. */
|
|
35
|
+
const lead = useRef('');
|
|
36
|
+
/** Ostatnio zgłoszony zestaw odrzuconych barw — żeby nie powtarzać tego samego ostrzeżenia. */
|
|
37
|
+
const reported = useRef('');
|
|
38
|
+
/** Tło po adaptacji. Puste znaczy „nie ma czego podmieniać" i wtedy liczy się samo `background`. */
|
|
39
|
+
const [adapted, setAdapted] = useState('');
|
|
40
|
+
/**
|
|
41
|
+
* Ton i kolor treści dobrane do malowanego tła. Puste znaczy „sekcja nie ma własnego tła"
|
|
42
|
+
* i wtedy treść zostaje przy `--kpt-color-on-surface` ze stylu.
|
|
43
|
+
*/
|
|
44
|
+
const [ink, setInk] = useState({ tone: '', color: '' });
|
|
45
|
+
const rate = useRef(speed);
|
|
46
|
+
rate.current = speed;
|
|
47
|
+
const [onScreen, setOnScreen] = useState(true);
|
|
48
|
+
const [reduced, setReduced] = useState(false);
|
|
49
|
+
/** Licznik zmian motywu; podbicie każe przeliczyć kolory z tokenów na nowo. */
|
|
50
|
+
const [theme, setTheme] = useState(0);
|
|
51
|
+
const paint = useCallback(() => {
|
|
52
|
+
if (context.current)
|
|
53
|
+
renderer.current?.draw(context.current, clock.current);
|
|
54
|
+
}, []);
|
|
55
|
+
const measure = useCallback(() => {
|
|
56
|
+
const element = canvas.current;
|
|
57
|
+
if (!element)
|
|
58
|
+
return;
|
|
59
|
+
context.current ??= element.getContext('2d');
|
|
60
|
+
const rect = element.getBoundingClientRect();
|
|
61
|
+
const dpr = Math.min(globalThis.devicePixelRatio || 1, KPT_BACKDROP_MAX_DPR);
|
|
62
|
+
const width = Math.round(rect.width);
|
|
63
|
+
const height = Math.round(rect.height);
|
|
64
|
+
size.current = { width, height };
|
|
65
|
+
element.width = Math.round(width * dpr);
|
|
66
|
+
element.height = Math.round(height * dpr);
|
|
67
|
+
// Zapis width/height zeruje kontekst, więc skala musi wrócić po każdym pomiarze.
|
|
68
|
+
context.current?.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
69
|
+
}, []);
|
|
70
|
+
// Tablica barw przychodzi nowa przy każdym renderze rodzica, więc do zależności idzie jej treść.
|
|
71
|
+
const ramp = colors.join('|');
|
|
72
|
+
const list = useMemo(() => (ramp === '' ? EMPTY : ramp.split('|')), [ramp]);
|
|
73
|
+
// Każda zmiana wejść buduje renderer od nowa — zamiast doklejać ścieżki „co się zmieniło".
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
// Kolory rozwiązuje się na canvasie, nie na hoście: sonda `var()` pisze inline style, a nasłuch
|
|
76
|
+
// motywu ogląda `style` hosta i jego przodków — zapis na hoście zapętliłby przebudowy.
|
|
77
|
+
const scope = canvas.current;
|
|
78
|
+
if (!scope)
|
|
79
|
+
return undefined;
|
|
80
|
+
measure();
|
|
81
|
+
const resolved = resolveBackdropColors(scope, color, list, adaptive);
|
|
82
|
+
lead.current = resolved.color;
|
|
83
|
+
const painted = adaptive ? adaptBackdropValue(background, readBackdropSurface(scope)) : background;
|
|
84
|
+
setAdapted(adaptive ? painted : '');
|
|
85
|
+
// Ton liczymy z tła, które sekcja naprawdę pomaluje — czyli już po adaptacji.
|
|
86
|
+
setInk(readBackdropInk(scope, painted));
|
|
87
|
+
// Efekt chodzi przy każdej zmianie wejść, więc ciągnięcie suwaka nad wadliwą rampą zasypałoby
|
|
88
|
+
// konsolę tym samym zdaniem — zgłaszamy dopiero zestaw inny niż poprzednio.
|
|
89
|
+
const rejected = resolved.invalid.join(', ');
|
|
90
|
+
if (rejected !== reported.current) {
|
|
91
|
+
reported.current = rejected;
|
|
92
|
+
if (rejected !== '' && isDevBuild()) {
|
|
93
|
+
console.warn(`[KptBackdrop] Pominięto barwy, których przeglądarka nie rozpoznaje: ${rejected}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const current = createBackdropRenderer({
|
|
97
|
+
effect,
|
|
98
|
+
color: resolved.color,
|
|
99
|
+
colors: resolved.colors,
|
|
100
|
+
anchor,
|
|
101
|
+
cell,
|
|
102
|
+
gap,
|
|
103
|
+
density,
|
|
104
|
+
falloff,
|
|
105
|
+
rotation,
|
|
106
|
+
band,
|
|
107
|
+
amplitude,
|
|
108
|
+
seed,
|
|
109
|
+
});
|
|
110
|
+
current.resize(size.current.width, size.current.height);
|
|
111
|
+
renderer.current = current;
|
|
112
|
+
paint();
|
|
113
|
+
return () => {
|
|
114
|
+
current.dispose();
|
|
115
|
+
renderer.current = null;
|
|
116
|
+
};
|
|
117
|
+
}, [effect, color, list, anchor, cell, gap, density, falloff, rotation, band, amplitude, seed, adaptive, background, theme, measure, paint]);
|
|
118
|
+
// Sam rozmiar nie zmienia ustawień, więc renderer zostaje — przelicza tylko geometrię.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const element = canvas.current;
|
|
121
|
+
if (!element)
|
|
122
|
+
return undefined;
|
|
123
|
+
const observer = new ResizeObserver(() => {
|
|
124
|
+
const rect = element.getBoundingClientRect();
|
|
125
|
+
if (Math.round(rect.width) === size.current.width && Math.round(rect.height) === size.current.height)
|
|
126
|
+
return;
|
|
127
|
+
measure();
|
|
128
|
+
renderer.current?.resize(size.current.width, size.current.height);
|
|
129
|
+
paint();
|
|
130
|
+
});
|
|
131
|
+
observer.observe(element);
|
|
132
|
+
return () => observer.disconnect();
|
|
133
|
+
}, [measure, paint]);
|
|
134
|
+
// Sekcja poza ekranem i karta w tle to ten sam przypadek: nie ma czego oglądać.
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const element = host.current;
|
|
137
|
+
if (!element)
|
|
138
|
+
return undefined;
|
|
139
|
+
const update = (visible) => setOnScreen(visible && !document.hidden);
|
|
140
|
+
const observer = new IntersectionObserver((entries) => update(entries.some((entry) => entry.isIntersecting)));
|
|
141
|
+
observer.observe(element);
|
|
142
|
+
const onTabSwitch = () => setOnScreen(!document.hidden);
|
|
143
|
+
document.addEventListener('visibilitychange', onTabSwitch);
|
|
144
|
+
return () => {
|
|
145
|
+
observer.disconnect();
|
|
146
|
+
document.removeEventListener('visibilitychange', onTabSwitch);
|
|
147
|
+
};
|
|
148
|
+
}, []);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const query = matchMedia('(prefers-reduced-motion: reduce)');
|
|
151
|
+
setReduced(query.matches);
|
|
152
|
+
const onChange = (event) => setReduced(event.matches);
|
|
153
|
+
query.addEventListener('change', onChange);
|
|
154
|
+
return () => query.removeEventListener('change', onChange);
|
|
155
|
+
}, []);
|
|
156
|
+
// Zmiana motywu przestawia tokeny, a kolory renderera są już wyliczone — trzeba je odczytać
|
|
157
|
+
// na nowo. Nasłuch trafia też w zmiany niezwiązane z barwą, więc licznik podbija się dopiero
|
|
158
|
+
// wtedy, gdy odczyt naprawdę dał inny kolor.
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const element = host.current;
|
|
161
|
+
if (!element)
|
|
162
|
+
return undefined;
|
|
163
|
+
return observeBackdropTheme(element, () => {
|
|
164
|
+
const scope = canvas.current;
|
|
165
|
+
if (!scope)
|
|
166
|
+
return;
|
|
167
|
+
// Ten sam rachunek co w efekcie budującym — inaczej strażnik porównywałby wartość sprzed
|
|
168
|
+
// adaptacji z zapamiętaną po adaptacji i podbijałby licznik bez przerwy.
|
|
169
|
+
const next = adaptive ? adaptBackdropValue(color, readBackdropSurface(scope)) : color;
|
|
170
|
+
if (readBackdropLead(scope, next) === lead.current)
|
|
171
|
+
return;
|
|
172
|
+
setTheme((count) => count + 1);
|
|
173
|
+
});
|
|
174
|
+
}, [color, adaptive]);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (paused || reduced || !onScreen || speed <= 0)
|
|
177
|
+
return undefined;
|
|
178
|
+
let frame = 0;
|
|
179
|
+
let last = 0;
|
|
180
|
+
const tick = (now) => {
|
|
181
|
+
const step = last === 0 ? 0 : Math.min(now - last, KPT_BACKDROP_MAX_STEP);
|
|
182
|
+
last = now;
|
|
183
|
+
// Prędkość wchodzi w zegar, nie w fazę — suwak przyspiesza obraz zamiast go przeskakiwać.
|
|
184
|
+
clock.current += (step / 1000) * rate.current;
|
|
185
|
+
paint();
|
|
186
|
+
frame = requestAnimationFrame(tick);
|
|
187
|
+
};
|
|
188
|
+
frame = requestAnimationFrame(tick);
|
|
189
|
+
return () => cancelAnimationFrame(frame);
|
|
190
|
+
}, [paused, reduced, onScreen, speed, paint]);
|
|
191
|
+
const { className: contentClassName, ...contentRest } = contentProps ?? {};
|
|
192
|
+
/** Tło malowane na hoście: po adaptacji, jeśli jest, inaczej to, co podano. */
|
|
193
|
+
const surface = adapted || background;
|
|
194
|
+
return (_jsxs("div", { ref: host, className: cn('kpt-backdrop-host', className), "data-effect": effect, "data-tone": ink.tone || undefined, style: { ...(surface ? { background: surface } : null), ...(ink.color ? { color: ink.color } : null), ...style }, ...rest, children: [_jsx("canvas", { ref: canvas, className: "kpt-backdrop__canvas", "aria-hidden": "true" }), _jsx("div", { className: cn('kpt-backdrop__content', contentClassName), ...contentRest, children: children })] }));
|
|
195
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# KptBackdrop (@konce-pt/react)
|
|
2
|
+
|
|
3
|
+
A decorative background layer for a section: the component wraps your content and paints an
|
|
4
|
+
animated canvas beneath it. You reach for it where a flat surface looks unfinished, and where
|
|
5
|
+
a video or a large image would cost bandwidth and refuse to follow the theme.
|
|
6
|
+
Children sit a layer above the canvas and behave normally.
|
|
7
|
+
Import: `import { KptBackdrop } from '@konce-pt/react';`
|
|
8
|
+
|
|
9
|
+
## DOM
|
|
10
|
+
<div class="kpt-backdrop-host" data-effect="wave">
|
|
11
|
+
<canvas class="kpt-backdrop__canvas" aria-hidden="true"></canvas>
|
|
12
|
+
<div class="kpt-backdrop__content">…</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
## Props (shared)
|
|
16
|
+
- `effect`: 'signal-grid' | 'wave' (default 'signal-grid')
|
|
17
|
+
- `background`: string — the section background; empty takes `--kpt-color-surface`
|
|
18
|
+
- `speed`: number (default 1) — 1 is the calm pace of either effect, 0 freezes the picture
|
|
19
|
+
- `seed`: number (default 0) — the same value gives the same layout and the same sway
|
|
20
|
+
- `paused`: boolean — stops the animation, leaving the last frame
|
|
21
|
+
- `adaptive`: boolean — carries hardcoded colours onto the current theme's surface
|
|
22
|
+
- `contentProps` — props for the `.kpt-backdrop__content` layer
|
|
23
|
+
- `className`, `ref` and every other `<div>` prop go to `.kpt-backdrop-host`
|
|
24
|
+
|
|
25
|
+
## Props (signal-grid)
|
|
26
|
+
- `color`: string — the square colour, any CSS notation, tokens included; empty takes
|
|
27
|
+
`--kpt-color-primary`
|
|
28
|
+
- `anchor`: 'left' | 'right' (default 'right') — the edge the grid is densest at
|
|
29
|
+
- `cell`: number (default 5), `gap`: number (default 11) — the square and the space between, in px
|
|
30
|
+
- `density`: number (default 0.8) — the share of lit cells at the anchor, 0–1
|
|
31
|
+
- `falloff`: number (default 1.4) — how fast the grid fades towards the far edge
|
|
32
|
+
|
|
33
|
+
## Props (wave)
|
|
34
|
+
- `colors`: string[] — the colour ramp the current slides along, closed into a loop; every
|
|
35
|
+
stop takes any CSS notation, `color`'s rules exactly
|
|
36
|
+
- `rotation`: number (default 12) — the angle the current crosses the frame at, in degrees
|
|
37
|
+
- `band`: number (default 0.34) — the band's width as a fraction of the frame's height
|
|
38
|
+
- `amplitude`: number (default 0.16) — how far the centre line sways
|
|
39
|
+
|
|
40
|
+
A prop that does not belong to the current effect is simply not read.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
<KptBackdrop effect="wave" rotation={18} colors={['#5B8AF2', '#A86DF2', '#F25B9E']}>
|
|
44
|
+
<h1>Build your app from the first prompt</h1>
|
|
45
|
+
<KptButton>Get started</KptButton>
|
|
46
|
+
</KptBackdrop>
|
|
47
|
+
|
|
48
|
+
## The two effects
|
|
49
|
+
`signal-grid` is a fine grid of squares that quietly twinkle, dense at one edge and fading to
|
|
50
|
+
empty at the other; the gradient comes from **how many** squares are lit, not from opacity laid
|
|
51
|
+
over an even grid. `wave` is one soft current of light with hues sliding along it and no hard edge
|
|
52
|
+
anywhere; `rotation` turns it, `band` widens it, `amplitude` sways it.
|
|
53
|
+
|
|
54
|
+
Without `colors` the wave's ramp is `--kpt-color-primary` plus two hues derived from it by
|
|
55
|
+
rotation. The library's palette is neutral, so that default current is single-hued and muted.
|
|
56
|
+
|
|
57
|
+
Every stop is put to the browser, so `oklch()`, `color-mix()`, `rgb()` and named colours are read
|
|
58
|
+
the same as hex; alpha is dropped. A stop the browser refuses falls out of the ramp, and if every
|
|
59
|
+
one of them falls out the default ramp takes over rather than the section going blank — a dev
|
|
60
|
+
build says in the console which stops it dropped.
|
|
61
|
+
|
|
62
|
+
## The same picture in both ports
|
|
63
|
+
Everything that decides how it looks — the grid layout, the density ramp, the twinkle, the current
|
|
64
|
+
and the colour ramp, plus the renderers themselves — lives in `@konce-pt/backdrop`, shared with
|
|
65
|
+
the Angular port. This component is the lifecycle: `rAF`, `ResizeObserver`, `IntersectionObserver`,
|
|
66
|
+
the reduced-motion query and the theme watch. The same props therefore give the
|
|
67
|
+
same frame in either port.
|
|
68
|
+
|
|
69
|
+
## Rest
|
|
70
|
+
Drawing stops when there is nothing to watch: the section is off-screen, the tab is in the
|
|
71
|
+
background, `paused` is set, `speed` is 0, or the system asks for reduced motion. What is left is
|
|
72
|
+
a single static frame, not emptiness. `speed` feeds the clock rather than the phase, so a speed
|
|
73
|
+
slider accelerates the picture instead of jumping it. A change of `colors` is tracked by content,
|
|
74
|
+
not by array identity, so a fresh array from a parent render does not rebuild the renderer.
|
|
75
|
+
|
|
76
|
+
## Accessibility
|
|
77
|
+
The canvas carries `aria-hidden="true"` and `pointer-events: none` — it is decoration with no
|
|
78
|
+
content and nothing to hit. Children keep their own roles and tab order.
|
|
79
|
+
|
|
80
|
+
## Tokens
|
|
81
|
+
`--kpt-color-primary` (squares and the default ramp), `--kpt-color-surface` (background),
|
|
82
|
+
`--kpt-color-on-surface`. Section-wide overrides: `--kpt-backdrop-color`, `--kpt-backdrop-surface`.
|
|
83
|
+
|
|
84
|
+
## Adaptive colours
|
|
85
|
+
A literal stays put by design, which is right for a brand colour and wrong for a section meant to
|
|
86
|
+
read in both themes. `adaptive` closes that gap: it carries `color`, `colors` and `background`
|
|
87
|
+
onto the surface of the current theme, keeping each one's distance from that surface. Black picked
|
|
88
|
+
for a white section comes out white on a near-black ground, and that white background comes out at
|
|
89
|
+
the surface's own lightness. Only lightness moves — hue and chroma stay, so a brand colour keeps
|
|
90
|
+
being that colour and only changes how light it is.
|
|
91
|
+
|
|
92
|
+
It touches literals only. An empty input takes a token, and a value reaching through `var()`
|
|
93
|
+
reaches a token too; both already follow the theme, and adapting them would turn them back again.
|
|
94
|
+
Values written for the light theme are the assumption — the library keeps light in `:root` and
|
|
95
|
+
dark as the override.
|
|
96
|
+
|
|
97
|
+
## Content on a custom background
|
|
98
|
+
The host paints `--kpt-color-surface` and lets content inherit `--kpt-color-on-surface`, which is
|
|
99
|
+
right until the section gets a `background` of its own — then that token describes the page around
|
|
100
|
+
it, not the ground the content actually sits on. A dark section in a light theme would come out
|
|
101
|
+
black on black.
|
|
102
|
+
|
|
103
|
+
So a section with its own background works out its own ink: the host sets `color` to match what it
|
|
104
|
+
paints, and announces the result as `data-tone` (`light` for a light background, `dark` for a dark
|
|
105
|
+
one). Text follows by inheritance; anything needing more than a text colour — buttons in the
|
|
106
|
+
section, say — can hang its own tokens off the attribute:
|
|
107
|
+
|
|
108
|
+
:where(div.kpt-backdrop-host[data-tone='light']) .hero { --kpt-button-filled-bg: #15181a; }
|
|
109
|
+
|
|
110
|
+
The tone is computed from the background the section really paints, so with `adaptive` on it
|
|
111
|
+
matches the adapted value, not the one written in the template. A section without its own
|
|
112
|
+
background announces nothing and content stays on `--kpt-color-on-surface`.
|
|
113
|
+
|
|
114
|
+
## Theme
|
|
115
|
+
Canvas does not expand `var()`, so the colour is read once and baked into the renderer — the
|
|
116
|
+
component therefore watches for anything that can restyle the tokens under it and reads them
|
|
117
|
+
again. `[data-theme="dark"]` is not tied to `:root`, so the watch covers `data-theme`, `class`
|
|
118
|
+
and `style` on every ancestor from the host up to `<html>`, plus the `prefers-color-scheme` query
|
|
119
|
+
for an application that keeps its dark tokens in a media query. A runtime override of
|
|
120
|
+
`--kpt-color-primary` is picked up the same way as a theme switch. The renderer is rebuilt only
|
|
121
|
+
when the colour actually came out different, so an unrelated class landing on `<html>` costs one
|
|
122
|
+
cheap read and nothing else.
|
|
123
|
+
|
|
124
|
+
Whether an explicit `color` or `colors` follows the theme is up to how it is written. A literal
|
|
125
|
+
stays put — `#5b8af2` is `#5b8af2` in either theme, which is what someone spelling out a brand
|
|
126
|
+
colour wants. An expression reaching for a token is resolved against the tree on every rebuild, so
|
|
127
|
+
it moves with the theme on its own:
|
|
128
|
+
|
|
129
|
+
<KptBackdrop color="color-mix(in oklab, #5b8af2, var(--kpt-color-surface) 30%)">
|
|
130
|
+
|
|
131
|
+
`var(--kpt-color-primary)` works the same way. A name that resolves to nothing falls back to the
|
|
132
|
+
token for `color`, and drops out of the ramp for `colors`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
|
+
import type { KptChatAuthor, KptChatMessage as KptChatMessageModel, KptChatMessageRow, KptChatReactionOption, KptChatReplyRef } from '@konce-pt/chat';
|
|
3
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
4
|
+
/** Wybór reakcji na wiadomości — komponent mówi, w co kliknięto, dopisanie należy do aplikacji. */
|
|
5
|
+
export interface KptChatReactEvent<T = unknown> {
|
|
6
|
+
message: KptChatMessageModel<T>;
|
|
7
|
+
emoji: string;
|
|
8
|
+
}
|
|
9
|
+
export interface KptChatProps<T = unknown> extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
|
|
10
|
+
/** Wiadomości w kolejności chronologicznej — komponent ich nie sortuje. */
|
|
11
|
+
messages?: readonly KptChatMessageModel<T>[];
|
|
12
|
+
/** `id` nadawcy uznawanego za „mnie": jego wiadomości idą na prawo i mają status. */
|
|
13
|
+
currentUser?: string;
|
|
14
|
+
/** Przerwa łamiąca grupę wiadomości jednego nadawcy. */
|
|
15
|
+
groupGapMs?: number;
|
|
16
|
+
/** `id` pierwszej nieprzeczytanej wiadomości — nad nią stanie dzielnik. */
|
|
17
|
+
unreadFrom?: string | null;
|
|
18
|
+
/** Brak połączenia: pasek u góry listy z liczbą wiadomości czekających na wysłanie. */
|
|
19
|
+
offline?: boolean;
|
|
20
|
+
/** Osoby piszące w tej chwili. */
|
|
21
|
+
typing?: readonly KptChatAuthor[];
|
|
22
|
+
/** Dosuwanie widoku do najnowszej wiadomości, dopóki użytkownik nie odjedzie w górę. */
|
|
23
|
+
autoScroll?: boolean;
|
|
24
|
+
/** Akcje bąbla pod kursorem: odpowiedz i zareaguj. */
|
|
25
|
+
actions?: boolean;
|
|
26
|
+
/** Zestaw wybieraka reakcji, przekazywany bąblom. Domyślnie pięć wbudowanych. */
|
|
27
|
+
reactionOptions?: readonly KptChatReactionOption[];
|
|
28
|
+
/** Własny komunikat pustej rozmowy. */
|
|
29
|
+
emptyLabel?: string;
|
|
30
|
+
onRetry?: (message: KptChatMessageModel<T>) => void;
|
|
31
|
+
onReply?: (message: KptChatMessageModel<T>) => void;
|
|
32
|
+
onReact?: (event: KptChatReactEvent<T>) => void;
|
|
33
|
+
onQuoted?: (quote: KptChatReplyRef) => void;
|
|
34
|
+
/** Przewinięcie na sam szczyt — moment na dociągnięcie starszych wiadomości. */
|
|
35
|
+
onLoadMore?: () => void;
|
|
36
|
+
/** Szablon treści bąbla — odpowiednik `<ng-template let-message>` z Angulara. */
|
|
37
|
+
renderMessage?: (message: KptChatMessageModel<T>, row: KptChatMessageRow<T>) => ReactNode;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Lista rozmowy — grupuje wiadomości, wstawia separatory dni i dzielnik nieprzeczytanych,
|
|
41
|
+
* trzyma widok przy dole i pokazuje pasek offline z liczbą wiadomości czekających na wysłanie.
|
|
42
|
+
*
|
|
43
|
+
* Sama logika wierszy mieszka w `@konce-pt/chat` (`buildChatRows`), więc port Angulara liczy ją
|
|
44
|
+
* tak samo. Komponent nie ma nic wspólnego z transportem: wiadomości dostaje, a `onRetry`
|
|
45
|
+
* i `onLoadMore` tylko zgłasza — ponowieniem wysyłki i dociąganiem historii zajmuje się aplikacja.
|
|
46
|
+
*
|
|
47
|
+
* Kompozytor (`KptChatComposer`) jest osobnym komponentem i stoi obok listy — dzięki temu
|
|
48
|
+
* czat bez pola wpisywania (podgląd, archiwum) nie wozi go ze sobą.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* <KptChat messages={messages} currentUser="me" typing={typing} onRetry={resend} />
|
|
52
|
+
*/
|
|
53
|
+
export declare function KptChat<T = unknown>({ messages, currentUser, groupGapMs, unreadFrom, offline, typing, autoScroll, actions, reactionOptions, emptyLabel, onRetry, onReply, onReact, onQuoted, onLoadMore, renderMessage, locale, dictionary, className, ...rest }: Readonly<KptChatProps<T>>): import("react").JSX.Element;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { KPT_CHAT_GROUP_GAP_MS, KPT_CHAT_REACTIONS, buildChatRows, chatDayIndicator, pendingCount } from '@konce-pt/chat';
|
|
4
|
+
import { cn } from "../internal/cn.js";
|
|
5
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
6
|
+
import { KptIcon } from "../icon/icon.js";
|
|
7
|
+
import { KptChatMessage } from "../chat-message/chat-message.js";
|
|
8
|
+
import { KptChatTyping } from "../chat-typing/chat-typing.js";
|
|
9
|
+
/** Odległość od dołu (px), przy której listę uznajemy jeszcze za przypiętą. */
|
|
10
|
+
const PIN_THRESHOLD = 48;
|
|
11
|
+
/**
|
|
12
|
+
* Wiersze, które nie mogą znaleźć się pod pływającą pigułką dnia: separatory i komunikaty
|
|
13
|
+
* systemowe, czyli wszystko wyśrodkowane, co czyta się jak jej bliźniak. Bąble są poza listą —
|
|
14
|
+
* pigułka nad wypowiedzią to normalny obrazek czatu.
|
|
15
|
+
*/
|
|
16
|
+
const BLOCKING_ROWS = '.kpt-chat__date, .kpt-chat__unread, .kpt-chat-message__system';
|
|
17
|
+
/**
|
|
18
|
+
* Lista rozmowy — grupuje wiadomości, wstawia separatory dni i dzielnik nieprzeczytanych,
|
|
19
|
+
* trzyma widok przy dole i pokazuje pasek offline z liczbą wiadomości czekających na wysłanie.
|
|
20
|
+
*
|
|
21
|
+
* Sama logika wierszy mieszka w `@konce-pt/chat` (`buildChatRows`), więc port Angulara liczy ją
|
|
22
|
+
* tak samo. Komponent nie ma nic wspólnego z transportem: wiadomości dostaje, a `onRetry`
|
|
23
|
+
* i `onLoadMore` tylko zgłasza — ponowieniem wysyłki i dociąganiem historii zajmuje się aplikacja.
|
|
24
|
+
*
|
|
25
|
+
* Kompozytor (`KptChatComposer`) jest osobnym komponentem i stoi obok listy — dzięki temu
|
|
26
|
+
* czat bez pola wpisywania (podgląd, archiwum) nie wozi go ze sobą.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <KptChat messages={messages} currentUser="me" typing={typing} onRetry={resend} />
|
|
30
|
+
*/
|
|
31
|
+
export function KptChat({ messages = [], currentUser = '', groupGapMs = KPT_CHAT_GROUP_GAP_MS, unreadFrom = null, offline = false, typing = [], autoScroll = true, actions = false, reactionOptions = KPT_CHAT_REACTIONS, emptyLabel = '', onRetry, onReply, onReact, onQuoted, onLoadMore, renderMessage, locale, dictionary, className, ...rest }) {
|
|
32
|
+
const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
|
|
33
|
+
const viewportRef = useRef(null);
|
|
34
|
+
const dayRef = useRef(null);
|
|
35
|
+
/** Czy widok stoi przy dole listy. Odjazd w górę wyłącza dosuwanie. */
|
|
36
|
+
const [pinned, setPinned] = useState(true);
|
|
37
|
+
/** Ile wiadomości przyszło, odkąd użytkownik odjechał w górę. */
|
|
38
|
+
const [newCount, setNewCount] = useState(0);
|
|
39
|
+
/** Tekst pływającej pigułki dnia — zostaje po zgaszeniu, żeby jej pas nie zmieniał wysokości. */
|
|
40
|
+
const [dayLabel, setDayLabel] = useState('');
|
|
41
|
+
/** Czy pigułka dnia jest widoczna. */
|
|
42
|
+
const [dayVisible, setDayVisible] = useState(false);
|
|
43
|
+
const lastCount = useRef(0);
|
|
44
|
+
const wasAtTop = useRef(false);
|
|
45
|
+
const rows = buildChatRows(messages, { currentUserId: currentUser, gapMs: groupGapMs, unreadFrom });
|
|
46
|
+
const pending = pendingCount(messages);
|
|
47
|
+
/**
|
|
48
|
+
* Ustawia tekst pływającej pigułki dnia — co ma na niej być, liczy `chatDayIndicator`
|
|
49
|
+
* z `@konce-pt/chat`, tutaj zostaje czytanie prostokątów i przepisanie etykiety.
|
|
50
|
+
*
|
|
51
|
+
* Wszystkie pomiary idą przed zapisem stanu, więc przeglądarka przelicza układ raz
|
|
52
|
+
* na klatkę zamiast przy każdym wierszu.
|
|
53
|
+
*/
|
|
54
|
+
const syncDay = useCallback(() => {
|
|
55
|
+
const element = viewportRef.current;
|
|
56
|
+
const pill = dayRef.current;
|
|
57
|
+
if (!element || !pill)
|
|
58
|
+
return;
|
|
59
|
+
const separators = Array.from(element.querySelectorAll('.kpt-chat__date'));
|
|
60
|
+
const blockers = Array.from(element.querySelectorAll(BLOCKING_ROWS));
|
|
61
|
+
const box = (node) => {
|
|
62
|
+
const rect = node.getBoundingClientRect();
|
|
63
|
+
return { top: rect.top, bottom: rect.bottom };
|
|
64
|
+
};
|
|
65
|
+
const active = chatDayIndicator(separators.map(box), blockers.map(box), box(pill));
|
|
66
|
+
setDayVisible(active >= 0);
|
|
67
|
+
if (active < 0)
|
|
68
|
+
return;
|
|
69
|
+
// Etykieta zostaje po zgaszeniu pigułki: pusta zmieniałaby jej wysokość, a skrypt mierzy
|
|
70
|
+
// ten pas także wtedy, gdy pigułki nie widać.
|
|
71
|
+
setDayLabel(separators[active]?.textContent?.trim() ?? '');
|
|
72
|
+
}, []);
|
|
73
|
+
const scrollToBottom = (behavior) => {
|
|
74
|
+
const element = viewportRef.current;
|
|
75
|
+
if (!element)
|
|
76
|
+
return;
|
|
77
|
+
const reduced = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
78
|
+
element.scrollTo({ top: element.scrollHeight, behavior: behavior ?? (reduced ? 'auto' : 'smooth') });
|
|
79
|
+
};
|
|
80
|
+
// Dosunięcie liczone po ułożeniu, nie po malowaniu — inaczej widać skok listy.
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
if (!viewportRef.current)
|
|
83
|
+
return;
|
|
84
|
+
const count = messages.length;
|
|
85
|
+
const grew = count > lastCount.current;
|
|
86
|
+
const first = lastCount.current === 0;
|
|
87
|
+
lastCount.current = count;
|
|
88
|
+
if (!grew)
|
|
89
|
+
return;
|
|
90
|
+
// Pierwsze wejście zawsze ląduje na dole — rozmowę czyta się od końca.
|
|
91
|
+
if (first || (autoScroll && pinned))
|
|
92
|
+
scrollToBottom(first ? 'auto' : undefined);
|
|
93
|
+
else
|
|
94
|
+
setNewCount((value) => value + 1);
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [messages]);
|
|
97
|
+
// Po każdej zmianie listy wiersze stoją gdzie indziej — pigułkę trzeba przeliczyć, inaczej
|
|
98
|
+
// pokazywałaby dzień sprzed dojścia nowej wiadomości.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
syncDay();
|
|
101
|
+
}, [messages, offline, activeLocale, syncDay]);
|
|
102
|
+
// To samo po zmianie szerokości: zawinięty tekst przesuwa wiersze spod pigułki.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const element = viewportRef.current;
|
|
105
|
+
if (!element || !globalThis.ResizeObserver)
|
|
106
|
+
return;
|
|
107
|
+
const observer = new ResizeObserver(() => syncDay());
|
|
108
|
+
observer.observe(element);
|
|
109
|
+
return () => observer.disconnect();
|
|
110
|
+
}, [syncDay]);
|
|
111
|
+
const onScroll = (element) => {
|
|
112
|
+
const distance = element.scrollHeight - element.scrollTop - element.clientHeight;
|
|
113
|
+
const atBottom = distance < PIN_THRESHOLD;
|
|
114
|
+
setPinned(atBottom);
|
|
115
|
+
if (atBottom)
|
|
116
|
+
setNewCount(0);
|
|
117
|
+
// `onLoadMore` tylko na wejściu w strefę szczytu — przy każdym zdarzeniu scroll
|
|
118
|
+
// aplikacja dostawałaby dziesiątki żądań na jedno przewinięcie.
|
|
119
|
+
const atTop = element.scrollTop < PIN_THRESHOLD;
|
|
120
|
+
if (atTop && !wasAtTop.current)
|
|
121
|
+
onLoadMore?.();
|
|
122
|
+
wasAtTop.current = atTop;
|
|
123
|
+
syncDay();
|
|
124
|
+
};
|
|
125
|
+
const jumpToLatest = () => {
|
|
126
|
+
setPinned(true);
|
|
127
|
+
setNewCount(0);
|
|
128
|
+
scrollToBottom();
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Etykieta separatora dnia. Rdzeń zwraca rodzaj, słownik daje „dziś"/„wczoraj",
|
|
132
|
+
* a nazwę dnia i pełną datę — `Intl`, żeby nie trzymać w słowniku kalendarza.
|
|
133
|
+
*/
|
|
134
|
+
const dateText = (label) => {
|
|
135
|
+
switch (label.kind) {
|
|
136
|
+
case 'today':
|
|
137
|
+
return t('chat.today');
|
|
138
|
+
case 'yesterday':
|
|
139
|
+
return t('chat.yesterday');
|
|
140
|
+
case 'weekday':
|
|
141
|
+
return new Intl.DateTimeFormat(activeLocale, { weekday: 'long' }).format(label.date);
|
|
142
|
+
default:
|
|
143
|
+
return new Intl.DateTimeFormat(activeLocale, {
|
|
144
|
+
day: 'numeric',
|
|
145
|
+
month: 'long',
|
|
146
|
+
year: 'numeric',
|
|
147
|
+
}).format(label.date);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
return (_jsxs("div", { className: cn('kpt-chat-host', className), ...rest, children: [offline ? (_jsxs("p", { className: "kpt-chat__offline", role: "status", children: [_jsx(KptIcon, { name: "alert-triangle" }), _jsx("span", { children: t('chat.offline') }), pending ? (_jsx("span", { className: "kpt-chat__offline-count", children: t('chat.pending', { count: pending }) })) : null] })) : null, _jsxs("div", { className: "kpt-chat__body", children: [_jsxs("div", { ref: viewportRef, className: "kpt-chat__viewport", role: "log", "aria-live": "polite", "aria-relevant": "additions", onScroll: (event) => onScroll(event.currentTarget), children: [rows.length === 0 ? (_jsx("p", { className: "kpt-chat__empty", children: emptyLabel || t('chat.empty') })) : null, rows.map((row) => {
|
|
151
|
+
if (row.kind === 'date') {
|
|
152
|
+
const label = dateText(row.label);
|
|
153
|
+
return (_jsx("p", { className: "kpt-chat__date", role: "separator", "aria-label": label, children: label }, row.key));
|
|
154
|
+
}
|
|
155
|
+
if (row.kind === 'unread') {
|
|
156
|
+
return (_jsx("p", { className: "kpt-chat__unread", role: "separator", "aria-label": t('chat.newMessages', { count: row.count }), children: _jsx("span", { children: t('chat.unread') }) }, row.key));
|
|
157
|
+
}
|
|
158
|
+
return (_jsx(KptChatMessage, { message: row.message, text: renderMessage ? '' : null, align: row.own ? 'end' : 'start', position: row.position, showAuthor: row.showAuthor, showAvatar: row.showAvatar, showMeta: row.showMeta, actions: actions, reactionOptions: reactionOptions, locale: locale, dictionary: dictionary, onRetry: () => onRetry?.(row.message), onReply: () => onReply?.(row.message), onReact: (emoji) => onReact?.({ message: row.message, emoji }), onQuoted: (quote) => onQuoted?.(quote), children: renderMessage ? renderMessage(row.message, row) : null }, row.key));
|
|
159
|
+
}), typing.length ? _jsx(KptChatTyping, { authors: typing, locale: locale, dictionary: dictionary }) : null] }), _jsx("p", { ref: dayRef, className: "kpt-chat__day", "aria-hidden": "true", "data-hidden": dayVisible ? undefined : '', children: dayLabel }), newCount ? (_jsxs("button", { type: "button", className: "kpt-chat__jump", onClick: jumpToLatest, children: [_jsx(KptIcon, { name: "arrow-down" }), t('chat.newMessages', { count: newCount })] })) : null] })] }));
|
|
160
|
+
}
|