@konce-pt/angular 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/CHANGELOG.md CHANGED
@@ -1,5 +1,385 @@
1
1
  # @konce-pt/angular
2
2
 
3
+ ## 0.8.5
4
+
5
+ ### Minor Changes
6
+
7
+ - f615e93: Nowe wejście `adaptive` w `KptBackdrop`: barwy wpisane na sztywno przenoszą się na powierzchnię
8
+ bieżącego motywu.
9
+
10
+ Literał z założenia stoi w miejscu i tak ma być — `#5b8af2` to barwa marki, a nie sugestia.
11
+ Ale sekcja z `color="#000000"` na `background="#ffffff"` w motywie ciemnym zostawała białą kartą
12
+ wyciętą w ciemnej stronie, razem z treścią dobraną do tamtego tła. Do tej pory jedynym wyjściem
13
+ było przepisanie wejść na tokeny; teraz wystarczy `[adaptive]="true"`.
14
+
15
+ Adaptacja przenosi `color`, `colors` i `background` na powierzchnię bieżącego motywu, zachowując
16
+ odległość każdej barwy od tej powierzchni: czerń dobrana do bieli wychodzi bielą na tle
17
+ `neutral-950`, a białe tło wychodzi w jasności samej powierzchni. Rusza wyłącznie jasność — `a`
18
+ i `b` w OKLab zostają nietknięte, więc odcień i nasycenie przeżywają i barwa marki nie zmienia się
19
+ w inną barwę, tylko dobiera jasność do tła. Jasność wypychająca wynik poza sRGB zostaje przycięta
20
+ w drodze powrotnej, co kosztuje około trzech stopni odcienia i dziesiątą część nasycenia; za tłem
21
+ sekcji to niewidoczne, a własne mapowanie gamutu byłoby kosztem bez pokrycia.
22
+
23
+ Rusza wyłącznie literały. Puste wejście bierze token, a zapis przez `var()` też sięga po token —
24
+ jedno i drugie już idzie za motywem i adaptacja odwróciłaby je drugi raz, czyli z powrotem.
25
+ Punktem odniesienia jest motyw jasny, bo biblioteka trzyma jasny w `:root`, a ciemny jako
26
+ nadpisanie; to jedyne założenie, jakie ta zamiana robi.
27
+
28
+ W rdzeniu doszły `adaptLightness()` i `adaptToSurface()` — czysta arytmetyka z testami
29
+ `node --test` — oraz `readBackdropSurface()` i `adaptBackdropValue()` spinające je z drzewem.
30
+ Strażnik przebudowy liczy teraz kolor wiodący tą samą drogą co budowa; inaczej porównywałby
31
+ wartość sprzed adaptacji z zapamiętaną po adaptacji i przebudowywał renderer bez końca.
32
+
33
+ - f615e93: Sekcja z własnym tłem dobiera kolor treści do tego tła i ogłasza ton atrybutem `data-tone`.
34
+
35
+ Host malował `--kpt-color-surface` i zostawiał treści `--kpt-color-on-surface`. To jest słuszne,
36
+ dopóki sekcja nie dostanie własnego `background` — wtedy ten token opisuje stronę dookoła, a nie
37
+ to, na czym treść naprawdę leży. Sekcja z ciemnym tłem w motywie jasnym miała `color` prawie czarny
38
+ na prawie czarnym tle, czyli treść niewidoczną. Każdy konsument musiał to obchodzić sam; demo
39
+ playgroundu obchodziło, licząc jasność tła po swojej stronie, więc błąd nie był widoczny akurat
40
+ tam, gdzie się go szuka.
41
+
42
+ Teraz komponent liczy to sam: `color` na hoście idzie pod tło, które sekcja naprawdę maluje,
43
+ a wynik wychodzi na zewnątrz jako `data-tone` (`light` przy jasnym tle, `dark` przy ciemnym).
44
+ Napis bierze kolor dziedziczeniem, a to, czemu sam kolor tekstu nie wystarcza — przyciski i inne
45
+ komponenty w sekcji — podwiesza własne tokeny pod atrybut:
46
+
47
+ :where(kpt-backdrop[data-tone='light']) .hero { --kpt-button-filled-bg: #15181a; }
48
+
49
+ Ton liczy się po adaptacji, więc pod `adaptive` zgadza się z tłem malowanym, a nie z wpisanym
50
+ w szablonie. Sekcja bez własnego `background` nie ogłasza nic i treść zostaje na
51
+ `--kpt-color-on-surface` — dla niej nic się nie zmienia.
52
+
53
+ O kontraście rozstrzyga jasność względna WCAG (`relativeLuminance`, `prefersDarkContent`
54
+ w rdzeniu, z testami), a nie jasność OKLab: pierwsza mówi, ile z barwy wychodzi światła, druga jak
55
+ jasno barwa wygląda, i tylko na pierwszej stoi rachunek kontrastu.
56
+
57
+ Playground dostał przełącznik „Idź za motywem" dla `adaptive` i przestał liczyć jasność tła po
58
+ swojej stronie — podpina się pod `data-tone` komponentu, więc jedna polityka zamiast dwóch.
59
+
60
+ - f615e93: Rampa `colors` w `KptBackdrop` przyjmuje dowolny zapis CSS, nie tylko heksadecymalny.
61
+
62
+ Wstęga miesza barwy po biegunowej postaci OKLab, ale dostawały się tam dotąd wyłącznie przystanki
63
+ sparsowane przez `parseHexColor` — czyli `#rgb` i `#rrggbb`. `oklch()`, `color-mix()`, `rgb()`,
64
+ nazwy własne i `var(--kpt-*)` wypadały z rampy, a gdy wypadły wszystkie, renderer schodził do
65
+ `IDLE` i **kadr zostawał pusty bez żadnego sygnału**. Tym dotkliwiej, że rampa domyślna — ta,
66
+ której się nie podaje — od początku szła przez `readCssColor` i rozumiała każdy zapis, bo tokeny
67
+ biblioteki są zapisane w OKLCH. Podana rampa miała gorszy parser niż niepodana.
68
+
69
+ Teraz obie idą tą samą drogą: o znaczenie każdego przystanku pytamy przeglądarkę, a do renderera
70
+ trafia już sam hex — jego kontrakt się nie zmienia. Przystanek, którego przeglądarka nie przyjmuje,
71
+ wypada z rampy; gdy wypadną wszystkie, wchodzi rampa wyprowadzona z koloru wiodącego, zamiast gasić
72
+ sekcję. Najczęstsza pomyłka, `var(--kpt-color-primary)` w rampie, i tak celuje w ten właśnie kolor,
73
+ więc użytkownik dostaje niemal to, o co mu chodziło, plus jedno ostrzeżenie w konsoli w buildzie
74
+ dev. Wartość alfa wypada — tablica barw wstęgi jest RGB.
75
+
76
+ `readCssColor()` zwraca `null` na wartości, której przeglądarka nie uznaje za kolor. Dotąd dawała
77
+ czerń, bo `fillStyle` odrzuca taki zapis po cichu i zostaje przy poprzednim — nie dało się odróżnić
78
+ śmiecia od legalnej czerni ani o czymkolwiek ostrzec. Rozstrzyga to para różnych wartowników: przy
79
+ wartości nieprawidłowej każdy zostaje na swoim miejscu i oba przebiegi się rozchodzą. Przy okazji
80
+ sonda jest czyszczona przed każdym odczytem — barwa z kanałem alfa mieszała się z tym, co zostawiło
81
+ poprzednie wywołanie.
82
+
83
+ - f615e93: Wejścia koloru `KptBackdrop` rozumieją `var()`, więc barwa podana w szablonie może iść za motywem.
84
+
85
+ Kolor wiodący był dotąd brany z wejścia bez zmian, a sonda 1×1 jest oderwana od drzewa — zapis
86
+ `color="var(--kpt-color-primary)"` nie miał prawa zadziałać i lądował w ostrzeżeniu. Zamrożenie
87
+ literału jest poprawne (`#5b8af2` ma zostać `#5b8af2`), ale odcinało jedyny zapis, który sam
88
+ wiedziałby, co zrobić przy przełączeniu motywu: wyrażenie odwołujące się do tokenów.
89
+
90
+ Doszedł `resolveCssValue()`: wyrażenie trafia do własności-sondy na elemencie w drzewie, a z jego
91
+ wyliczonego stylu wraca już z podstawionym `var()`. Dalej idzie do `readCssColor`, bo `color-mix()`
92
+ zostaje w custom property niewyliczone, a canvas je wylicza. Dzięki temu
93
+
94
+ <kpt-backdrop color="color-mix(in oklab, #5b8af2, var(--kpt-color-surface) 30%)">
95
+
96
+ daje kwadraciki, które jaśnieją na jasnej powierzchni i ciemnieją na ciemnej, przy jednej wartości
97
+ w szablonie — przeliczane istniejącym już nasłuchem motywu. To samo działa dla przystanków rampy
98
+ `colors`. Biblioteka nie zgaduje przy tym, jaki kolor jest „odpowiedni" w drugim motywie: decyduje
99
+ autor wyrażenia. Zapis bez `var()` omija całą ścieżkę, więc literał nie płaci za nic.
100
+
101
+ Sonda musi pisać poza zasięgiem nasłuchu motywu, bo ten ogląda `style` hosta i jego przodków —
102
+ zapis na hoście budziłby nasłuch, ten sięgałby po kolor na nowo i pętla nie miałaby końca. Stąd
103
+ `resolveBackdropColors()` i `readBackdropLead()` dostają teraz canvas komponentu, a nie hosta;
104
+ tokeny czytają się z niego tak samo, bo `--_dot` dziedziczy się w dół.
105
+
106
+ Nazwa zmiennej, która nic nie daje, schodzi przy `color` do tokenu — tego chce autor literówki —
107
+ a przy `colors` wypada z rampy i trafia do istniejącego ostrzeżenia w buildzie dev.
108
+
109
+ - c1f8885: Nowe animowane tło `KptBackdrop` (`kpt-backdrop`) i pod nim nowa paczka `@konce-pt/backdrop` —
110
+ rdzeń pure-TS z testami `node --test`. Komponent jest opakowaniem sekcji: canvas leży pod spodem,
111
+ rzutowana treść warstwę wyżej i zachowuje się zwyczajnie, więc pod hero z nagłówkiem i przyciskami
112
+ nie trzeba układać niczego ręcznie.
113
+
114
+ Efekt `signal-grid` to drobna siatka kwadratów, które cicho migoczą — gęsta przy krawędzi wskazanej
115
+ przez `anchor`, wygasająca do pustki przy przeciwnej. Gradient bierze się z tego, **ile** kwadratów
116
+ się pali, a nie z przezroczystości nałożonej na równomierną siatkę: to różnica między czymś, co
117
+ czyta się jak sygnał, a fakturą z maską na wierzchu. Sterują tym `density` (udział zapalonych
118
+ komórek przy kotwicy) i `falloff` (jak szybko to gaśnie), a rozstawem `cell` i `gap`.
119
+
120
+ Każda komórka wylicza swoje losowania — czy się pali, fazę i tempo migotania — z własnej kolumny
121
+ i wiersza przez `kptHash01()`. Siatka nie może trzymać stanu między klatkami, bo przy zmianie
122
+ rozmiaru komórki wędrują i tablica `Math.random()` rozsypałaby cały obraz; przy losowaniu
123
+ z pozycji układ przeżywa zmianę rozmiaru okna zamiast tasować się od nowa. Tempa są rozstrzelone
124
+ w przedziale 0,45–1,55, bo przy jednym tempie cała siatka pulsuje zgodnie i zamiast migotania
125
+ wychodzi oddech. Siatka celowo wychodzi poza obszar, a nadmiar jest ucinany — siatka kończąca się
126
+ przed krawędzią wygląda na uciętą.
127
+
128
+ Rysowanie stoi, gdy nie ma czego oglądać: sekcja poza ekranem (`IntersectionObserver`), karta w tle,
129
+ ustawione `paused`, `speed` równe zeru albo `prefers-reduced-motion`, o który komponent pyta sam.
130
+ Zostaje wtedy jedna statyczna klatka ze zróżnicowaną jasnością, nie pustka — efekt jest częścią
131
+ kompozycji, nie ozdobą. `speed` wchodzi w zegar (`clock += krok * speed`), a nie w fazę, więc suwak
132
+ prędkości przyspiesza obraz zamiast go przeskakiwać.
133
+
134
+ Kolor kwadratów bierze się z `--kpt-color-primary`, tło z `--kpt-color-surface`; oba da się nadpisać
135
+ wejściami `color` i `background` albo własnościami `--kpt-backdrop-color` i `--kpt-backdrop-surface`,
136
+ bo sekcja promocyjna bywa w kolorach spoza palety aplikacji. Canvas nie rozwija `var()`, więc token
137
+ odczytuje się z wyliczonego stylu hosta i na nowo po zmianie motywu. Canvas nosi `aria-hidden="true"`
138
+ i `pointer-events: none` — to dekoracja bez treści i bez niczego do trafienia.
139
+
140
+ Rdzeń (`buildSignalGrid`, `signalGridAlpha`, `signalGridDensity`, `kptHash01`) jest re-eksportowany
141
+ z `@konce-pt/angular`, więc aplikacja nie instaluje paczki samodzielnie, a ten sam efekt da się
142
+ narysować własnym rendererem. Port Reacta dostanie komponent osobno.
143
+
144
+ - c1f8885: Drugi efekt tła — `wave` — i `KptBackdrop` w porcie Reacta.
145
+
146
+ `wave` to jedna miękka wstęga światła płynąca przez kadr, z barwami przesuwającymi się wzdłuż niej
147
+ i bez twardej krawędzi w całym obrazie. Linia środkowa jest sumą harmonicznych o niewspółmiernych
148
+ częstotliwościach, więc prąd faluje bez powtarzalnego wzoru; jasność opada od niej krzywą Gaussa
149
+ i to opadanie odróżnia prąd światła od paska. Nowe wejścia: `colors` (rampa barw), `rotation`
150
+ (kąt, pod jakim prąd przecina kadr — odpowiednik `anchor` z signal-grid, tyle że płynny), `band`
151
+ i `amplitude`. Wejście nienależące do bieżącego efektu po prostu nie jest czytane.
152
+
153
+ Rampa barw jest zamknięta w pętlę i mieszana po **biegunowej** postaci OKLab. Interpolacja sRGB
154
+ miesza wartości po gammie i wychodzi błotem, a nawet prostokątny OKLab przecina oś achromatyczną
155
+ dla przeciwnych odcieni: w połowie drogi z niebieskiego w żółty zostaje szarość dokładnie tam,
156
+ gdzie prąd świeci najmocniej. Odcień idzie krótszym łukiem, a przystanek bez nasycenia przejmuje
157
+ odcień sąsiada, bo `atan2` na zerowym nasyceniu zwraca kąt przypadkowy. Bez podanego `colors` rampą
158
+ jest `--kpt-color-primary` i dwie barwy wyprowadzone z niego obrotem odcienia — paleta biblioteki
159
+ jest neutralna, więc sięganie po tokeny statusowe do celów dekoracyjnych byłoby użyciem semantyki
160
+ nie na miejscu.
161
+
162
+ Wstęga liczy się w buforze o dłuższym boku 160 px i jest skalowana w górę: miękkość bierze się
163
+ z interpolacji przeglądarki, więc kilka tysięcy pikseli na klatkę obsługuje kadr dowolnej
164
+ wielkości. Krótszy bok ma osobną podłogę 72 próbek — przy proporcjonalnym skalowaniu szeroki kadr
165
+ miałby kilkadziesiąt wierszy i prąd obrócony blisko kąta prostego łamałby się właśnie wzdłuż tej
166
+ osi.
167
+
168
+ **Renderery przeniosły się do `@konce-pt/backdrop`.** Rysowanie nie potrzebuje frameworka, tylko
169
+ `CanvasRenderingContext2D`, więc `createBackdropRenderer()` (plus `resolveBackdropColors()`
170
+ i stałe cyklu życia) mieszka teraz w rdzeniu, a komponent w obu portach jest samym cyklem życia:
171
+ rAF, `ResizeObserver`, `IntersectionObserver`, zapytanie o ograniczony ruch i obserwator motywu.
172
+ Te same wejścia dają dzięki temu tę samą klatkę w Angularze i w Reakcie. Doszedł też
173
+ `readCssColor()` — tokeny są zapisane w OKLCH, a rampę trzeba wymieszać, nie tylko podać dalej,
174
+ więc o znaczenie zapisu pytamy przeglądarkę, malując kolor na canvasie 1×1.
175
+
176
+ `@konce-pt/react` dostaje `KptBackdrop` z kompletem propsów obu efektów, ten sam DOM i ten sam
177
+ arkusz `components.css`. Zmianę `colors` śledzi po treści tablicy, nie po jej tożsamości, więc
178
+ nowa tablica z renderu rodzica nie przebudowuje renderera.
179
+
180
+ - ce34ab3: Nowa rodzina `KptChat` w subpathu `@konce-pt/angular/chat`: `kpt-chat` (lista rozmowy),
181
+ `kpt-chat-message` (bąbel), `kpt-chat-typing` (wskaźnik pisania), `kpt-chat-composer` (pole
182
+ pisania) i `kpt-chat-dock` (pływający panel w rogu, zbudowany na `kpt-fab`). Subpath, nie rdzeń —
183
+ aplikacje bez czatu nie mają powodu pobierać docka razem z logiką breakpointów.
184
+
185
+ Nowa paczka `@konce-pt/chat` — rdzeń pure-TS z testami `node --test`. `buildChatRows()` zamienia
186
+ płaską listę wiadomości na wiersze do wyrenderowania: separatory dni, dzielnik nieprzeczytanych
187
+ i pozycję w grupie, z której wynika zapadnięty narożnik, ogonek, awatar i wiersz z godziną.
188
+ Grupa łamie się na zmianie autora, przerwie powyżej pięciu minut, granicy doby kalendarzowej,
189
+ wiadomości systemowej i dzielniku nieprzeczytanych. Doba liczona jest od północy do północy,
190
+ a nie odejmowaniem znaczników czasu — doba ze zmianą czasu ma 23 albo 25 godzin, więc „wczoraj
191
+ o 23:30" wypadałoby raz do roku o dzień obok.
192
+
193
+ Nieudana wysyłka umie o sobie powiedzieć: `status: 'failed'` obwodzi bąbel kolorem
194
+ `--kpt-color-danger-border` i stawia strzałkę ponowienia **poza** nim, po zewnętrznej stronie
195
+ listy. Taki bąbel pokazuje wiersz meta nawet w środku grupy — strzałka bez niczego, co ją
196
+ tłumaczy, byłaby zagadką. Pasek `[offline]` liczy wiadomości czekające na wysłanie przez
197
+ `pendingCount()`. Potwierdzenia z serwera przychodzą nie po kolei, więc `mergeStatus()` zostawia
198
+ wyższy stopień: spóźnione „dostarczone" nie cofnie `read`. Wyjątkiem jest `failed`, który wygrywa
199
+ zawsze — wiadomość odrzucona po wysłaniu nie może zostać na zawsze „wysłana".
200
+
201
+ Transport zostaje w aplikacji: komponent przyjmuje wiadomości i tylko zgłasza `(retry)`
202
+ oraz `(loadMore)`.
203
+
204
+ Bąble są rozłożone pod monochromatyczny motyw biblioteki: różnicę robi jasność, nie odcień.
205
+ Wiadomość przychodząca siada na `--kpt-color-muted`, własna domyślnie na pełnym
206
+ `--kpt-color-primary`. `--kpt-color-surface-variant` dzieli od tła listy 1,5% jasności, więc bąbel
207
+ przychodzący na nim po prostu znikał, a `--kpt-color-primary-subtle` ma dokładnie jasność
208
+ `--kpt-color-muted`, przez co własny i cudzy bąbel były nie do odróżnienia. Wariant `primary`
209
+ zostaje jako cichsza opcja dla własnych wiadomości; wariant `muted` zniknął, bo po tej zmianie
210
+ powielał `neutral`. Przyklejona pigułka separatora dnia przejeżdża nad wiadomościami, więc dostała
211
+ podniesione tło, ramkę i cień — bez nich zlewała się z bąblem, po którym właśnie jechała.
212
+
213
+ `kpt-scroll-top` przykleja się dokładnie w to samo miejsce co dock, z tym samym `--kpt-z-overlay`,
214
+ i połykał FAB czatu w całości — wystawała sama plakietka nieprzeczytanych, a przycisku nie dało się
215
+ kliknąć. Arkusz rozwiązuje to teraz sam: przy docku `bottom-end` przycisk powrotu na górę ustępuje
216
+ w bok o szerokość FAB-a. W bok, nie do góry, bo nad FAB-em stoi panel rozmowy — przesunięcie
217
+ w pionie zamieniłoby jedną kolizję na gorszą. Do tego `kpt-chat-dock` dostaje `offset` na wypadek
218
+ innych widgetów w tym rogu.
219
+
220
+ Słownik `@konce-pt/i18n` dostaje sekcję `chat` (EN i PL). Rdzeń zwraca **rodzaj** etykiety dnia
221
+ (`{ kind: 'today' }`), a nie napis — brzmienie należy do słownika, nazwa dnia tygodnia do `Intl`,
222
+ a paczka bez zależności nie ma po co wozić tłumaczeń.
223
+
224
+ Port Reacta dostaje subpath `@konce-pt/react/chat` z kompletem pięciu komponentów na tym samym
225
+ rdzeniu i tym samym arkuszu `components.css` — te same klasy `kpt-*` i te same atrybuty `data-*`,
226
+ więc oba porty renderują identycznie. `model()` z Angulara rozpada się na pary
227
+ controlled/uncontrolled (`value`/`defaultValue`/`onValueChange`, `open`/`defaultOpen`/`onOpenChange`),
228
+ a `<ng-template let-message>` na `renderMessage(message, row)`.
229
+
230
+ Zestaw wbudowany `@konce-pt/icons` rośnie z 86 do 92 ikon: `checks`, `paperclip`, `send`,
231
+ `message-circle`, `corner-up-left`, `mood-smile`. Bez `checks` nie dałoby się odróżnić
232
+ „dostarczono" od „wysłano", a komponenty biblioteki muszą działać bez `provideKptTablerIcons()`.
233
+
234
+ - 7626e71: `KptChip` dostaje wejście `textTransform` (`'none' | 'capitalize' | 'uppercase' | 'lowercase'`,
235
+ domyślnie `'none'`) w obu portach. Ląduje jako `data-text-transform` na `.kpt-chip` i tam ustawia
236
+ `text-transform`, więc etykieta zostaje w treści zapisana po swojemu, a o sposobie jej wyświetlenia
237
+ decyduje chip — pigułka nad nagłówkiem hero wygląda tak samo niezależnie od tego, czy tekst
238
+ przyszedł ze słownika, z CMS-a czy z API.
239
+
240
+ `capitalize` podnosi pierwszą literę każdego słowa — **także po dywizie** („Agent-Ready"), bo CSS
241
+ nie wie, gdzie kończy się wyraz złożony. Przy etykietach z myślnikiem pewniejszy jest zapis wprost
242
+ w treści; to samo ograniczenie ma każde `text-transform: capitalize`, a nie ta implementacja.
243
+
244
+ - a1c5468: `KptDataView` dostaje `pageSizeOptions`, a rozmiar strony staje się sterowalny — w Angularze
245
+ `pageSize` to teraz `model`, w React dochodzą `defaultPageSize` i `onPageSizeChange`. Do tej pory
246
+ komponent podawał paginatorowi `[pageSize()]` jako jedyną opcję, więc select „Wierszy" był martwą
247
+ kontrolką: pokazywał liczbę, której nie dało się zmienić. Pager zostaje widoczny także wtedy, gdy
248
+ wszystko mieści się na jednej stronie, o ile jest w czym wybierać — inaczej wybranie największego
249
+ rozmiaru chowałoby select, którego użytkownik właśnie użył.
250
+
251
+ Pasek paginatora w `KptDataView` znów stoi w jednym rzędzie po prawej. Opakowanie ustawiało
252
+ `.kpt-data-view .kpt-paginator { display: block }`, a selektor potomka bije `.kpt-paginator
253
+ { display: flex }` z tej samej warstwy — trzy bloki paginatora rozpadały się na trzy linie.
254
+
255
+ `KptDataView` pozwala ustawić gęstość siatki: `gridColumns` (dwukierunkowe), `gridColumnsOptions`
256
+ i `gridMinWidth`. Przy więcej niż jednej opcji w układzie siatki pasek narzędzi dostaje drugą grupę
257
+ segmentową z ikoną `grid-dots`. Wybrana liczba jest sufitem, nie sztywną liczbą — minimum ścieżki
258
+ rośnie do szerokości jednej z N kolumn, więc wąski kontener schodzi niżej, zamiast ściskać karty
259
+ poniżej `gridMinWidth`. Do tej pory o gęstości decydował wyłącznie arkusz aplikacji, bo komponent
260
+ miał `minmax(10rem, 1fr)` wpisane na sztywno.
261
+
262
+ Przyciski przełącznika układu niosą wreszcie `aria-pressed` — czytnik ekranu nie ogłaszał, który
263
+ układ jest włączony.
264
+
265
+ Przycisk siatki w przełączniku układu rysuje `layout-grid` zamiast `menu`: hamburger to
266
+ semantycznie lista, nie siatka. `layout-grid` wchodzi do zestawu wbudowanego `KPT_ICONS`
267
+ (86 ikon, razem z `grid-dots` dla paska gęstości), bo komponent biblioteki musi działać bez `provideKptTablerIcons()`.
268
+
269
+ - 0daa856: Nowa karta podglądu `KptPeek` (`kpt-peek` + `kptPeekTrigger`) i na niej wzmianki w tekście:
270
+ `KptMention` (pojedyncza wzmianka) oraz `KptMentions` (akapit rozbity na treść i wzmianki).
271
+ Obie rzeczy w rdzeniu biblioteki, nie w subpathu — karta jest mała i przyda się wszędzie tam,
272
+ gdzie tooltip za mało niesie, a popover za dużo zasłania.
273
+
274
+ `kptTooltip` ma `pointer-events: none`, więc linku w jego środku nie da się kliknąć, a `kpt-popover`
275
+ otwiera dopiero klik i kładzie na stronie backdrop. `kpt-peek` stoi pomiędzy: hover otwiera kartę
276
+ po `openDelay`, zjazd kursora zamyka po `closeDelay` (najazd na kartę anuluje zamknięcie, bo kursor
277
+ musi mieć czas przejechać z triggera na kartę), a treść karty jest w pełni interaktywna i niczego
278
+ nie przykrywa. Fokus otwiera bez zwłoki, a klik, Enter albo dotyk **przypina** kartę: zostaje mimo
279
+ zjazdu kursora, a fokus wchodzi do środka. To jedyna droga do treści karty tam, gdzie hovera nie ma
280
+ w ogóle, więc interaktywna karta zostaje osiągalna na dotyku i z klawiatury. Escape zamyka i oddaje
281
+ fokus triggerowi.
282
+
283
+ Nowa paczka `@konce-pt/mention` — rdzeń pure-TS z testami `node --test`. `parseMentionText()`
284
+ rozbija akapit na treść i wzmianki (`@id` albo `@[Nazwa ze spacją](id)`), `mentionQuery()` mówi,
285
+ czy kursor stoi w zapytaniu `@…`, a `filterMentions()` dopasowuje podpowiedzi. Sklejenie segmentów
286
+ wraca do oryginału znak w znak — podgląd rozjeżdżający się z zapisaną treścią byłby gorszy niż brak
287
+ podglądu. `mail@example.com` wzmianką nie jest: `@` musi stać na początku tekstu albo po znaku,
288
+ który nie jest literą, cyfrą ani podkreśleniem. Dopasowanie zdejmuje diakrytyki, a litery z kreską
289
+ mapuje ręcznie — `ł` to osobny znak Unicode, nie `l` ze znakiem diakrytycznym, więc samo NFD
290
+ zostawiłoby „Łukasza" poza zasięgiem zapytania „lukasz".
291
+
292
+ W treści zapisuje się **identyfikator**, nie nazwa: `id` przeżyje zmianę nazwiska, a etykieta
293
+ bierze się z listy pozycji. Identyfikator spoza listy też się renderuje — etykietą zostaje wtedy on
294
+ sam, bo notatka ma dać się przeczytać także wtedy, gdy ktoś odszedł z zespołu. `kpt-mentions` nie
295
+ renderuje HTML-a: akapit wchodzi jako zwykły napis, a wzmianka jest elementem, który biblioteka sama
296
+ zbudowała — aplikacja nie musi przepuszczać przez sanitizer treści pisanej przez użytkowników tylko
297
+ po to, żeby wyszły z niej wzmianki. Bez szablonu karty wzmianka jest wyróżnionym słowem, a nie
298
+ przyciskiem: nie ma czego otworzyć, więc niczego nie udaje.
299
+
300
+ `kpt-rich-text` dostaje `mentions`, `mentionLimit` i `(mentioned)`: napisanie `@` otwiera listę
301
+ podpowiedzi zaczepioną w miejscu kursora, strzałki po niej chodzą, Enter albo Tab wstawia, Escape
302
+ zamyka. Zapytanie czytamy z węzła tekstowego przy kursorze, nie z HTML-a — w treści siedzą tagi,
303
+ a `@` ma widzieć tylko to, co naprawdę napisano w tym miejscu. Wstawiona wzmianka to
304
+ `<span class="kpt-mention" data-mention="id" contenteditable="false">` plus twarda spacja:
305
+ identyfikator przeżywa obieg przez wartość, `contenteditable="false"` trzyma wzmiankę w jednym
306
+ kawałku (bez tego kursor wchodzi w środek nazwiska), a spacja daje mu gdzie stanąć za blokiem,
307
+ w który nie da się wejść. Oba atrybuty weszły na allowlistę sanitizera w obu portach — są bierne,
308
+ nie wykonują niczego i nie wskazują zasobów.
309
+
310
+ `KptIconButton` w porcie Angulara dostaje `ariaExpanded` — jak `ariaPressed` musi zjechać na
311
+ wewnętrzny `<button>`, bo host nie ma roli przycisku. Słownik `@konce-pt/i18n` dostaje
312
+ `richText.mentions` (EN i PL) na etykietę listy podpowiedzi.
313
+
314
+ Port Reacta dostaje komplet: `KptPeek`, `KptMention`, `KptMentions` i te same trzy propsy
315
+ w `KptRichText`, na tym samym rdzeniu i tym samym arkuszu `components.css`. Propsy wyzwalacza
316
+ `KptPeek` lądują na samym elemencie podanym w `trigger` (klon z doklejonymi handlerami), więc
317
+ `aria-expanded` siedzi tam, gdzie czytnik ekranu naprawdę je przeczyta.
318
+
319
+ - `KptPickList` i `KptOrderList` dostają przeciąganie i dwuklik — w obu portach.
320
+
321
+ Nowe wejście `dragdrop` jest domyślnie wyłączone, tak samo jak `reorderable` w tabeli danych:
322
+ przeciąganie to zachowanie, na które trzeba się zgodzić, a nie takie, które ma się włączyć samo
323
+ komuś, kto tylko wyświetla listę. Włączone, porządkuje pozycje wewnątrz panelu, a w pick-liście
324
+ przenosi je także między panelami, na wskazane miejsce. Upuszczenie na pusty obszar listy dopina
325
+ element na koniec.
326
+
327
+ Upuszczony element ląduje **na pozycji tego, na który go upuszczono** — nie zamienia się z nim
328
+ miejscami. To ten sam wzór co przestawianie kolumn w tabeli; zamiana miejscami wygląda przy
329
+ dłuższej liście na przypadek, bo element skacze w dwa miejsca naraz.
330
+
331
+ Dwuklik w pick-liście przerzuca pojedynczą pozycję na drugą stronę, w obie strony i niezależnie
332
+ od zaznaczenia. Nie wymaga `dragdrop`, bo dwa kliknięcia pod spodem i tak się znoszą.
333
+
334
+ Pod spodem natywny HTML5 drag and drop, nie CDK. Port React ma mieć to samo zachowanie i ten sam
335
+ arkusz stylów, a dla CDK nie byłoby tam odpowiednika — jedna implementacja zamiast dwóch
336
+ rozjeżdżających się. Przyciski zostają pełną ścieżką z klawiatury, bo przeciąganie nie ma
337
+ klawiaturowego odpowiednika i bez nich sortowanie stałoby się niedostępne.
338
+
339
+ ### Patch Changes
340
+
341
+ - f615e93: `KptBackdrop` idzie za tokenami, a nie za samym atrybutem motywu.
342
+
343
+ Canvas nie rozwija `var()`, więc kolor wiodący jest odczytywany raz i zapiekany w rendererze —
344
+ zmianę tokenów trzeba zauważyć i odczytać je na nowo. Dotąd nasłuch obejmował `data-theme`
345
+ i `class` na `<html>`, co wystarczało na przełącznik jasny/ciemny i zostawiało trzy dziury:
346
+ nadpisanie tokenu inline (konfigurator marki ustawia `--kpt-color-primary` na `<html>` — reszta
347
+ strony się przemalowywała, tło zostawało na starej barwie), motyw zakresowy (`[data-theme="dark"]`
348
+ nie jest przywiązany do `:root`, więc niesie go dowolny przodek) oraz aplikację trzymającą ciemne
349
+ tokeny w media query zamiast w atrybucie.
350
+
351
+ Nasłuch idzie teraz po łańcuchu od hosta aż do `<html>` i patrzy u każdego przodka na `data-theme`,
352
+ `class` i `style`, a obok tego na zapytanie `prefers-color-scheme`. Szersza sieć łapie też zmiany
353
+ niemające nic wspólnego z barwą — klasa nakładki na `<html>` przy otwarciu dialogu, dowolny zapis
354
+ inline — więc renderer przebudowuje się dopiero wtedy, gdy odczyt naprawdę dał inny kolor. Sam
355
+ strażnik jest tańszy niż dawna bezwarunkowa przebudowa: jeden `getComputedStyle`, bez sondy 1×1
356
+ i bez arytmetyki OKLab.
357
+
358
+ W rdzeniu doszły `readBackdropLead()` (sam kolor wiodący, wydzielony z `resolveBackdropColors()`)
359
+ i `observeBackdropTheme()` (nasłuch razem z odpięciem). Oba porty wołają to samo, bo rozjazd
360
+ znaczyłby, że ta sama sekcja reaguje na motyw inaczej w Angularze niż w Reakcie.
361
+
362
+ - Updated dependencies [f615e93]
363
+ - Updated dependencies [f615e93]
364
+ - Updated dependencies [f615e93]
365
+ - Updated dependencies [f615e93]
366
+ - Updated dependencies [c1f8885]
367
+ - Updated dependencies [f615e93]
368
+ - Updated dependencies [c1f8885]
369
+ - Updated dependencies [ce34ab3]
370
+ - Updated dependencies [a1c5468]
371
+ - Updated dependencies [0daa856]
372
+ - @konce-pt/backdrop@0.8.5
373
+ - @konce-pt/chat@0.8.5
374
+ - @konce-pt/icons@0.8.5
375
+ - @konce-pt/i18n@0.8.5
376
+ - @konce-pt/mention@0.8.5
377
+ - @konce-pt/chart@0.8.5
378
+ - @konce-pt/datetime@0.8.5
379
+ - @konce-pt/map@0.8.5
380
+ - @konce-pt/roadmap@0.8.5
381
+ - @konce-pt/table@0.8.5
382
+
3
383
  ## 0.8.3
4
384
 
5
385
  ### Patch Changes
@@ -0,0 +1,160 @@
1
+ # KptChat (kpt-chat, kpt-chat-message, kpt-chat-typing, kpt-chat-composer, kpt-chat-dock)
2
+
3
+ A conversation: a grouped message list, a typing indicator, a composer and a floating dock in the
4
+ corner of the screen. The row-building core — grouping, day separators, send-status arithmetic —
5
+ lives in `@konce-pt/chat`: plain TypeScript, tested, shared with the React port.
6
+
7
+ Import: `import { KptChat, KptChatComposer, KptChatDock, KptChatMessage, KptChatTyping } from '@konce-pt/angular/chat';`
8
+
9
+ The library covers presentation and the local state of the list. Transport — a socket, HTTP, retry
10
+ with backoff — stays in the application: the component takes messages and only reports `(retry)`
11
+ and `(loadMore)`.
12
+
13
+ ## KptChat — inputs / outputs
14
+ - `messages: readonly KptChatMessage<T>[]` — **in chronological order**; the component does not sort
15
+ - `currentUser: string` — the author id treated as "me": those messages go right and carry a status
16
+ - `groupGapMs: number` (default `KPT_CHAT_GROUP_GAP_MS`, 5 min) — the silence that breaks a group
17
+ - `unreadFrom: string | null` — the id of the first unread message; the divider goes above it
18
+ - `offline: boolean` — the warning strip with a count of messages waiting to be sent
19
+ - `typing: readonly KptChatAuthor[]` — who is typing right now
20
+ - `autoScroll: boolean` (default true), `actions: boolean`, `emptyLabel: string`
21
+ - `reactionOptions: readonly KptChatReactionOption[]` (default `KPT_CHAT_REACTIONS`) — passed to the bubbles
22
+ - `(retry)`, `(reply)` → `KptChatMessage<T>`; `(react)` → `KptChatReactEvent<T>` (`{ message, emoji }`);
23
+ `(quoted)` → `KptChatReplyRef`; `(loadMore)` → `void`, fired when the viewport reaches the top
24
+
25
+ ## Example
26
+ <kpt-chat
27
+ [messages]="messages()"
28
+ currentUser="me"
29
+ [typing]="typing()"
30
+ [offline]="offline()"
31
+ (retry)="resend($event)"
32
+ (loadMore)="loadOlder()"
33
+ />
34
+ <kpt-chat-composer allowAttachments [offline]="offline()" (send)="onSend($event)" />
35
+
36
+ The composer is a sibling, not a slot: a read-only conversation (an archive, a preview) should not
37
+ have to carry a text field it never shows.
38
+
39
+ ## Custom bubble content
40
+ An `<ng-template>` inside `<kpt-chat>` replaces the bubble's text; the context is the message as
41
+ `$implicit` and the whole row as `row`. The bubble, its grouping and its meta row stay in place —
42
+ only the content is yours.
43
+
44
+ <kpt-chat [messages]="messages()" currentUser="me">
45
+ <ng-template let-message><kpt-card>{{ message.data.title }}</kpt-card></ng-template>
46
+ </kpt-chat>
47
+
48
+ ## KptChatMessage — the bubble on its own
49
+ Works with a whole `[message]` object or with single inputs (`author`, `text`, `time`, `status`,
50
+ `attachments`, `reactions`, `replyTo`), and takes projected content, so it renders anything.
51
+ - `align`: 'start' | 'end' — someone else's message versus your own
52
+ - `variant`: 'neutral' | 'primary' | 'solid' | 'ghost' | 'system'
53
+ - `position`: 'single' | 'first' | 'middle' | 'last' — the place in a group
54
+ - `showAuthor` / `showAvatar` / `showMeta` / `tail` / `actions`: boolean
55
+ - `reactionOptions`: `readonly KptChatReactionOption[]` (default `KPT_CHAT_REACTIONS`) — the picker set
56
+
57
+ The default variant follows the alignment: your own message gets the full `solid`, someone else's
58
+ gets `neutral`. The theme is monochrome, so the difference between bubbles is lightness, not hue —
59
+ `--kpt-color-primary-subtle` has exactly the lightness of `--kpt-color-muted`, which would make the
60
+ two sides indistinguishable. For a quieter list set `variant="primary"` by hand.
61
+
62
+ ## Bubble actions: reply and react
63
+ `actions` puts two buttons beside the bubble, shown on hover and on focus. The arrow emits
64
+ `(reply)` with the whole message — `replyRefFrom()` from `@konce-pt/chat` turns it into the
65
+ `replyTo` the composer's quote bar wants. The smiley opens a picker of five reactions (like, heart,
66
+ smile, wow, sad — `KPT_CHAT_REACTIONS`, overridable through `reactionOptions`) and emits `(react)`
67
+ with the chosen emoji.
68
+
69
+ Neither one writes to the model: the library says what was clicked, the application decides what
70
+ the conversation looks like afterwards. `toggleReaction(message.reactions, emoji)` does that
71
+ arithmetic — it adds the reaction, takes it back on a second click and drops an entry that reaches
72
+ zero, keeping the order of the rest.
73
+
74
+ (reply)="replyTo.set(replyRefFrom($event))"
75
+ (react)="onReact($event)" // toggleReaction(item.reactions, $event.emoji)
76
+
77
+ The picker closes on Escape, on a click outside and on picking; an option already given is marked
78
+ `aria-pressed`, so it is visible which one the next click takes back. The retry arrow of a failed
79
+ message takes the place of both — a message that has not gone out has nothing to reply to yet.
80
+
81
+ ## Grouping is what the shape is made of
82
+ `position` decides which corner collapses, which bubble wears the tail, carries the avatar and shows
83
+ the clock. Inside a group the corner on the sender's side drops to `--kpt-radius-sm`, so the bubbles
84
+ read as one utterance. `kpt-chat` gets all of it from `buildChatRows()`; set it by hand only when
85
+ you build the list yourself.
86
+
87
+ A group breaks on a different author, a gap longer than `groupGapMs`, a calendar-day boundary, a
88
+ system message and the unread divider.
89
+
90
+ The bubble that wears the tail has a **square** bottom corner on that side — a rounded one curved
91
+ away from the tail and the list background showed through the gap, which made the tail read as a
92
+ separate arrow stuck next to the bubble rather than a part of it. Bubbles without a tail keep the
93
+ rounded corner.
94
+
95
+ ## One day pill, not a stack of them
96
+ The separators in the list travel with the content; which day the view is standing in is told by one
97
+ pill floating over the list (`.kpt-chat__day`, `aria-hidden`, outside the scrolling area). Sticky
98
+ separators looked right until the first collision: every next one pinned to the same edge, so a stack
99
+ of frames grew up there and the older, wider pills stuck out from behind the newest.
100
+
101
+ On every scroll (and after every re-render and resize) the component measures the separators, the
102
+ unread divider and the system notices, hands the rectangles to `chatDayIndicator()` from
103
+ `@konce-pt/chat` and swaps the pill's text. A centred label driving into the pill's band puts the
104
+ pill out — it is the pill that yields, never the content — and with a separator that is also the
105
+ handover, since the arriving "Today" says the same thing. Bubbles are not blockers: a pill over an
106
+ utterance is the normal chat picture. The pill keeps its last label while it is out, so its band does
107
+ not change height under the script that measures it.
108
+
109
+ ## An unsent message must be able to say so
110
+ `status: 'failed'` outlines the bubble with `--kpt-color-danger-border` and puts a retry arrow
111
+ **outside** it, on the outer side of the list. That bubble also shows its meta row even in the
112
+ middle of a group — the arrow with nothing to explain it would be a mystery.
113
+ Statuses: `sending`, `sent`, `delivered`, `read` (the same double check, in the accent colour) and
114
+ `failed`. Out-of-order acks are what `mergeStatus()` from `@konce-pt/chat` is for.
115
+
116
+ ## KptChatComposer
117
+ `[(value)]`, `placeholder`, `disabled`, `offline`, `allowAttachments`, `maxRows` (default 6),
118
+ `enterToSend` (default true), `[(replyTo)]`, `(send)` → `{ text, files, replyTo }`, `(typing)`.
119
+ Enter sends, Shift+Enter breaks the line; `enterToSend=false` swaps the two. The field grows with
120
+ the content up to `maxRows` and then scrolls.
121
+
122
+ It is **not** a Signal Forms control and will not become one: it never enters a form, has no
123
+ `touched`/`invalid` state, and its value disappears on send. `model('')` describes that honestly.
124
+
125
+ ## KptChatDock
126
+ `[(open)]`, `position` ('bottom-end' | 'bottom-start'), `unread`, `header`, `icon`, `width`,
127
+ `offset` (distance from the corner, both axes, default `--kpt-space-6`).
128
+
129
+ ## Sharing the corner with kpt-scroll-top
130
+ `kpt-scroll-top` pins itself to the same spot with the same `--kpt-z-overlay`, and it would swallow
131
+ the chat FAB whole — only the unread badge would stick out, and the button could not be clicked.
132
+ The stylesheet resolves it by itself: when a `bottom-end` dock is on the page, the scroll-top button
133
+ steps **aside** (`:root:has()` shifts it left by the width of the FAB plus a gap). Sideways, not up,
134
+ because the conversation panel stands above the FAB — moving it up would trade one collision for a
135
+ worse one, with the button landing on the messages.
136
+ It applies to `bottom-end` only, and it measures from the default offset: an application that sets
137
+ `offset` positions the scroll-top button itself.
138
+ The FAB is `kpt-fab` — the dock uses it, it does not replace it. The panel content is projected, so
139
+ the dock knows nothing about messages. Opening moves focus to the composer, `Escape` closes and
140
+ returns focus to the FAB. Below `--kpt-breakpoint-sm` the panel goes full-screen.
141
+
142
+ <kpt-chat-dock [(open)]="open" [unread]="unread()" header="Support">
143
+ <kpt-chat [messages]="messages()" currentUser="me" />
144
+ <kpt-chat-composer (send)="onSend($event)" />
145
+ </kpt-chat-dock>
146
+
147
+ ## Accessibility
148
+ The list is `role="log"` with `aria-live="polite"`, so a new message is announced without cutting
149
+ off what is being read. Each bubble is an `article` labelled with author, time and status — content
150
+ alone would say nothing about who wrote it or whether it was sent. The typing indicator is
151
+ `role="status"`; under `prefers-reduced-motion` the dots stop bouncing and pulse instead — they are
152
+ the only carrier of "someone is typing", so switching them off entirely would remove the message.
153
+
154
+ ## Tokens
155
+ Bubble `--kpt-color-muted` (incoming) / `--kpt-color-primary` + `--kpt-color-on-primary` (own);
156
+ failed `--kpt-color-danger-subtle` + `--kpt-color-danger-border`; offline strip
157
+ `--kpt-color-warning-subtle`; the day pill `--kpt-color-surface-raised` + a border and
158
+ `--kpt-elevation-1` (the same look in the list and floating above it — the floating one travels over
159
+ the bubbles and has to stand apart from them); dock `--kpt-elevation-3`
160
+ and `--kpt-z-overlay`.