@konce-pt/angular 0.8.3 → 0.9.0

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.
@@ -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`.