@better-zap/react 0.1.0 → 0.2.1
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 +529 -1
- package/package.json +56 -3
- package/dist/index.cjs +0 -687
- package/dist/index.d.cts +0 -167
- package/dist/index.d.mts +0 -167
- package/dist/index.mjs +0 -649
- package/dist/tailwind.css +0 -11
- package/dist/wpp-bg.webp +0 -0
package/README.md
CHANGED
|
@@ -1,3 +1,531 @@
|
|
|
1
1
|
# @better-zap/react
|
|
2
2
|
|
|
3
|
-
React UI components for Better Zap conversations and message views.
|
|
3
|
+
React UI components for Better Zap conversations and message views. The package ships presentational compound primitives for chat layout plus a domain-aware adapter for existing consumers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @better-zap/react
|
|
9
|
+
# or
|
|
10
|
+
npm install @better-zap/react
|
|
11
|
+
# or
|
|
12
|
+
yarn add @better-zap/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Requirement | Version |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `react` (peer) | `^19.0.0` |
|
|
18
|
+
| `react-dom` (peer) | `^19.0.0` |
|
|
19
|
+
| Node.js | `>=20` |
|
|
20
|
+
|
|
21
|
+
### Styling (Tailwind CSS v4)
|
|
22
|
+
|
|
23
|
+
Components are styled with Tailwind CSS utility classes; you need a **Tailwind CSS v4**
|
|
24
|
+
build pipeline in the consuming app. Add the package stylesheet to your global CSS:
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
@import "@better-zap/react/tailwind.css";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That file is not compiled CSS — it is a Tailwind v4 *source manifest* (an `@source "."`
|
|
31
|
+
directive) that tells Tailwind's JIT scanner where to find the utility classes used
|
|
32
|
+
inside `@better-zap/react`'s compiled output, so they get included in your build.
|
|
33
|
+
|
|
34
|
+
**Without a Tailwind v4 build, the components still function but render unstyled
|
|
35
|
+
markup** (no colors, spacing, or layout classes take effect).
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
A turnkey WhatsApp-style dashboard: a conversation list, a message pane with header
|
|
40
|
+
and message list, and a composer — wired together with `useState` for the selected
|
|
41
|
+
conversation.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
"use client";
|
|
45
|
+
|
|
46
|
+
import { useState } from "react";
|
|
47
|
+
import {
|
|
48
|
+
ConversationList,
|
|
49
|
+
MessageInput,
|
|
50
|
+
MessageList,
|
|
51
|
+
MessageView,
|
|
52
|
+
MessageViewContent,
|
|
53
|
+
MessageViewHeader,
|
|
54
|
+
WhatsappDashboard,
|
|
55
|
+
} from "@better-zap/react";
|
|
56
|
+
import type { Conversation, UIMessage } from "@better-zap/react";
|
|
57
|
+
|
|
58
|
+
function Dashboard({
|
|
59
|
+
conversations,
|
|
60
|
+
messagesByConversation,
|
|
61
|
+
onSend,
|
|
62
|
+
}: {
|
|
63
|
+
conversations: Conversation[];
|
|
64
|
+
messagesByConversation: Record<string, UIMessage[]>;
|
|
65
|
+
onSend: (conversationId: string, text: string) => void;
|
|
66
|
+
}) {
|
|
67
|
+
const [selectedId, setSelectedId] = useState<string | null>(
|
|
68
|
+
conversations[0]?.id ?? null,
|
|
69
|
+
);
|
|
70
|
+
const selected = conversations.find((c) => c.id === selectedId) ?? null;
|
|
71
|
+
const messages = selected ? messagesByConversation[selected.id] ?? [] : [];
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<WhatsappDashboard>
|
|
75
|
+
<ConversationList
|
|
76
|
+
conversations={conversations}
|
|
77
|
+
selectedConversationId={selectedId}
|
|
78
|
+
onSelect={setSelectedId}
|
|
79
|
+
/>
|
|
80
|
+
<MessageView>
|
|
81
|
+
<MessageViewHeader conversation={selected ?? undefined} />
|
|
82
|
+
<MessageViewContent>
|
|
83
|
+
<MessageList messages={messages} />
|
|
84
|
+
</MessageViewContent>
|
|
85
|
+
<MessageInput
|
|
86
|
+
onSend={(text) => selected && onSend(selected.id, text)}
|
|
87
|
+
conversation={selected}
|
|
88
|
+
/>
|
|
89
|
+
</MessageView>
|
|
90
|
+
</WhatsappDashboard>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`WhatsappDashboard` handles list/chat navigation on mobile viewports automatically;
|
|
96
|
+
`ConversationList`, `MessageView`, and `MessageInput` all work the same way whether
|
|
97
|
+
or not they're inside it (see the sections below).
|
|
98
|
+
|
|
99
|
+
## Two layers
|
|
100
|
+
|
|
101
|
+
The package has two layers: generic presentational **primitives** with no Better Zap
|
|
102
|
+
dependency, and Better Zap **adapters** that map domain data onto those primitives.
|
|
103
|
+
|
|
104
|
+
| Concern | Primitives (`Bubble*`, `Message*` layout, `Composer*`, `DateDivider`) | Adapters (`MessageBubble`, `MessageInput`, `MessageList`, `ConversationList`, `MessageView*`, `WhatsappDashboard`) |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| Data mapping (domain → presentation) | None — you pass `align`/`variant` directly | Own it — map `Conversation`/`UIMessage` fields (`sender`/`status`/`direction`) to presentational props |
|
|
107
|
+
| Row layout | `Message`, `MessageAvatar`, `MessageContent`, `MessageHeader`, `MessageFooter` | Composed internally by `MessageBubble` / `MessageList`'s default renderer |
|
|
108
|
+
| Bubble surface | `Bubble`, `BubbleContent`, `BubbleReactions` | Composed internally; still swappable via `renderMessage` (see [MessageList](#messagelist)) |
|
|
109
|
+
| Metadata / timestamps | You render them (`MessageFooter`, etc.) | Own default formatting (`formatDate`/`formatTime`), overridable via props |
|
|
110
|
+
| Actions | `ComposerButton`, `ComposerSend`, etc. — you wire callbacks | `MessageInput` gates action buttons on callbacks being provided (see [Actions and failures](#actions-and-failures)) |
|
|
111
|
+
| Responsive orchestration | None | `WhatsappDashboard` owns mobile list/chat navigation; `ConversationList`/`MessageView` read it optionally |
|
|
112
|
+
| Localization | Locale-free — you own all copy | pt-BR defaults (labels, `HOJE`/`ONTEM`, `HH:mm`), overridable via `labels`/`formatDate`/`formatTime` props |
|
|
113
|
+
|
|
114
|
+
See [Message vs Bubble](#message-vs-bubble) and [Composer vs MessageInput](#composer-vs-messageinput)
|
|
115
|
+
for the detailed anatomy of each pair.
|
|
116
|
+
|
|
117
|
+
## Subpath imports
|
|
118
|
+
|
|
119
|
+
The root entry (`@better-zap/react`) re-exports the full public surface and is a **client** boundary (aggregates client modules). Prefer leaf subpaths when you only need a slice of the UI — especially server components that should not pull virtualization or icon deps:
|
|
120
|
+
|
|
121
|
+
| Import | Boundary | Notes |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `@better-zap/react` | client | Full barrel |
|
|
124
|
+
| `@better-zap/react/bubble` | server-safe | Presentational bubble primitives |
|
|
125
|
+
| `@better-zap/react/message` | server-safe | Row layout primitives |
|
|
126
|
+
| `@better-zap/react/message-bubble` | server-safe | Domain `MessageBubble` adapter |
|
|
127
|
+
| `@better-zap/react/utils` | server-safe | `cn`, `getDisplayDate`, `renderSlot` |
|
|
128
|
+
| `@better-zap/react/composer` | client | Draft/send orchestration |
|
|
129
|
+
| `@better-zap/react/message-input` | client | Domain input + freeform window |
|
|
130
|
+
| `@better-zap/react/message-view` | client | Chat pane + `MessageList` |
|
|
131
|
+
| `@better-zap/react/conversation-list` | client | Virtualized sidebar |
|
|
132
|
+
| `@better-zap/react/whatsapp-dashboard` | client | Layout provider |
|
|
133
|
+
| `@better-zap/react/tailwind.css` | asset | Stylesheet |
|
|
134
|
+
|
|
135
|
+
Published client entries lead with `"use client"` in both ESM and CJS. There are **no** wildcard exports (`@better-zap/react/*` is not a public surface).
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
// Server Component — no client graph / no LegendList
|
|
139
|
+
import { Bubble, BubbleContent } from "@better-zap/react/bubble";
|
|
140
|
+
|
|
141
|
+
// Client Component entry
|
|
142
|
+
import { Composer, ComposerTextarea, ComposerSend } from "@better-zap/react/composer";
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Message vs Bubble
|
|
146
|
+
|
|
147
|
+
**Message** owns the row layout: alignment (`start` | `end`), optional avatar, header/footer slots, and metadata placement around the bubble.
|
|
148
|
+
|
|
149
|
+
**Bubble** owns the visible chat surface: presentational `variant` (`default` | `primary` | `destructive` | `outline` | `muted`), corner `align`, content, and optional reactions.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
Message — row: alignment, spacing
|
|
153
|
+
├── MessageAvatar — optional
|
|
154
|
+
└── MessageContent
|
|
155
|
+
├── MessageHeader — optional
|
|
156
|
+
├── Bubble — visible surface (variant + align)
|
|
157
|
+
│ ├── BubbleContent
|
|
158
|
+
│ └── BubbleReactions — optional
|
|
159
|
+
└── MessageFooter — optional
|
|
160
|
+
BubbleGroup / MessageGroup — consecutive-run stacking
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Minimal composition
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import {
|
|
167
|
+
Message,
|
|
168
|
+
MessageAvatar,
|
|
169
|
+
MessageContent,
|
|
170
|
+
MessageHeader,
|
|
171
|
+
MessageFooter,
|
|
172
|
+
Bubble,
|
|
173
|
+
BubbleContent,
|
|
174
|
+
BubbleReactions,
|
|
175
|
+
} from "@better-zap/react";
|
|
176
|
+
|
|
177
|
+
function Example() {
|
|
178
|
+
return (
|
|
179
|
+
<Message align="end">
|
|
180
|
+
<MessageAvatar>B</MessageAvatar>
|
|
181
|
+
<MessageContent>
|
|
182
|
+
<MessageHeader>Bot</MessageHeader>
|
|
183
|
+
<Bubble variant="primary" align="end">
|
|
184
|
+
<BubbleContent>Hello</BubbleContent>
|
|
185
|
+
<BubbleReactions>👍</BubbleReactions>
|
|
186
|
+
</Bubble>
|
|
187
|
+
<MessageFooter>12:34</MessageFooter>
|
|
188
|
+
</MessageContent>
|
|
189
|
+
</Message>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Adapter: `MessageBubble`
|
|
195
|
+
|
|
196
|
+
`MessageBubble` remains the Better Zap-aware compatibility component. It maps domain `sender` / `status` to presentational `align` / `variant`:
|
|
197
|
+
|
|
198
|
+
| Domain | Presentational |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| `sender="user"` | `align="start"`, `variant="default"` |
|
|
201
|
+
| `sender="bot"` (ok) | `align="end"`, `variant="primary"` |
|
|
202
|
+
| `status="failed"` | `align="end"`, `variant="destructive"` |
|
|
203
|
+
|
|
204
|
+
Prefer composing `Message` + `Bubble` for custom metadata placement, grouping, or interactive surfaces.
|
|
205
|
+
|
|
206
|
+
### Notes
|
|
207
|
+
|
|
208
|
+
- `BubbleGroup` / `MessageGroup` only stack children — they do **not** auto-adjust corner rounding.
|
|
209
|
+
- Interactive bubbles use `BubbleContent`'s `render` prop (Base UI-style element polymorphism), not `asChild`.
|
|
210
|
+
- Composition model follows shadcn-style compound parts: children + variants, no required React context between Message and Bubble.
|
|
211
|
+
|
|
212
|
+
## Composer vs MessageInput
|
|
213
|
+
|
|
214
|
+
**Composer** owns draft state and send orchestration via React context (`useComposer`).
|
|
215
|
+
Parts: `Composer`, `ComposerTextarea`, `ComposerSend`, `ComposerButton`, `ComposerError`.
|
|
216
|
+
|
|
217
|
+
**MessageInput** is the Better Zap domain adapter: freeform 24h window gating
|
|
218
|
+
(`useFreeformMessageWindow`), default pt-BR labels, optional action callbacks, and
|
|
219
|
+
the closed-window banner. Prefer `Composer*` when you need custom chrome or
|
|
220
|
+
controlled multi-conversation drafts.
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
MessageInput — domain adapter (window + labels + optional actions)
|
|
224
|
+
└── Composer — draft + send orchestration (context)
|
|
225
|
+
├── ComposerButton* — emoji/attach/mic only if callbacks provided
|
|
226
|
+
├── ComposerTextarea
|
|
227
|
+
├── ComposerSend
|
|
228
|
+
└── ComposerError
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Controlled multi-conversation drafts
|
|
232
|
+
|
|
233
|
+
Parent owns the draft map; `Composer` is controlled when `value !== undefined`:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
const [activeId, setActiveId] = useState(conversationId);
|
|
237
|
+
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
|
238
|
+
|
|
239
|
+
<Composer
|
|
240
|
+
value={drafts[activeId] ?? ""}
|
|
241
|
+
onValueChange={(next) =>
|
|
242
|
+
setDrafts((prev) => ({ ...prev, [activeId]: next }))
|
|
243
|
+
}
|
|
244
|
+
onSubmit={handleSend}
|
|
245
|
+
>
|
|
246
|
+
<ComposerTextarea aria-label="Mensagem" />
|
|
247
|
+
<ComposerSend aria-label="Enviar" />
|
|
248
|
+
</Composer>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Actions and failures
|
|
252
|
+
|
|
253
|
+
- Emoji / attach / mic on `MessageInput` render **only** when `onEmojiClick` /
|
|
254
|
+
`onAttachClick` / `onMicClick` are provided. Without callbacks they are omitted
|
|
255
|
+
(deliberate — no enabled inert controls).
|
|
256
|
+
- `onSubmit` / `onSend` may throw or reject; the draft is preserved, controls restore,
|
|
257
|
+
and `onError` / `onSendError` run. `send()` is fire-and-forget and never surfaces a
|
|
258
|
+
rejecting promise to click/keydown handlers.
|
|
259
|
+
- Freeform window: `useFreeformMessageWindow` schedules a timer at `expiresAt` and
|
|
260
|
+
`MessageInput` revalidates immediately before calling `onSend`.
|
|
261
|
+
|
|
262
|
+
### Client boundary
|
|
263
|
+
|
|
264
|
+
Published client entries (`composer`, `message-input`, and the root barrel)
|
|
265
|
+
lead with `"use client"` in both ESM and CJS. Prefer `@better-zap/react/composer`
|
|
266
|
+
or `@better-zap/react/message-input` when you want an explicit client boundary
|
|
267
|
+
without the full dashboard graph.
|
|
268
|
+
|
|
269
|
+
## ConversationList
|
|
270
|
+
|
|
271
|
+
**ConversationList** is a virtualized conversation sidebar with search, unread
|
|
272
|
+
filter chips, and row chrome. It works **standalone** or inside
|
|
273
|
+
`WhatsappDashboard`.
|
|
274
|
+
|
|
275
|
+
### Standalone vs dashboard
|
|
276
|
+
|
|
277
|
+
Outside a provider the list is always visible and selection only calls
|
|
278
|
+
`onSelect`. Inside `WhatsappDashboard`, selecting a row also sets mobile view to
|
|
279
|
+
`"chat"` (list hides on small viewports).
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// Standalone — no provider required
|
|
283
|
+
<ConversationList
|
|
284
|
+
conversations={conversations}
|
|
285
|
+
selectedConversationId={activeId}
|
|
286
|
+
onSelect={setActiveId}
|
|
287
|
+
/>
|
|
288
|
+
|
|
289
|
+
// Dashboard layout
|
|
290
|
+
<WhatsappDashboard>
|
|
291
|
+
<ConversationList conversations={...} onSelect={...} />
|
|
292
|
+
<MessageView>...</MessageView>
|
|
293
|
+
</WhatsappDashboard>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`useOptionalWhatsappDashboard()` returns the context value or `null` outside the
|
|
297
|
+
provider. Prefer `useWhatsappDashboard()` only when absence should throw.
|
|
298
|
+
|
|
299
|
+
### Controlled search and filter
|
|
300
|
+
|
|
301
|
+
Search/filter are controlled when `search` / `filter` are `!== undefined`
|
|
302
|
+
(same convention as Composer):
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
const [search, setSearch] = useState("");
|
|
306
|
+
const [filter, setFilter] = useState<"all" | "unread">("all");
|
|
307
|
+
|
|
308
|
+
<ConversationList
|
|
309
|
+
conversations={conversations}
|
|
310
|
+
search={search}
|
|
311
|
+
onSearchChange={setSearch}
|
|
312
|
+
filter={filter}
|
|
313
|
+
onFilterChange={setFilter}
|
|
314
|
+
/>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Uncontrolled defaults: `defaultSearch=""`, `defaultFilter="all"`. Change
|
|
318
|
+
callbacks still fire when uncontrolled if provided.
|
|
319
|
+
|
|
320
|
+
### Render seams and labels
|
|
321
|
+
|
|
322
|
+
- `renderItem(conversation, { isSelected, select })` — replace the whole row
|
|
323
|
+
(wins over `renderAvatar`).
|
|
324
|
+
- `renderAvatar(conversation)` — swap the default avatar inside
|
|
325
|
+
`ConversationItem`.
|
|
326
|
+
- `labels` — partial overrides for search, chips, loading/error/empty, preview
|
|
327
|
+
prefix, and `"Ontem"`.
|
|
328
|
+
- `formatTime(isoDate)` — optional time label override for rows.
|
|
329
|
+
|
|
330
|
+
## MessageList
|
|
331
|
+
|
|
332
|
+
**MessageList** is a virtualized, date-grouped message scroller (LegendList).
|
|
333
|
+
It works inside `MessageViewContent` (scroll context) or **standalone** with
|
|
334
|
+
direct `autoScroll` / `onScrollTop` props. Direct props win over context.
|
|
335
|
+
|
|
336
|
+
### Default use
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import { MessageList } from "@better-zap/react";
|
|
340
|
+
|
|
341
|
+
<MessageList
|
|
342
|
+
messages={messages}
|
|
343
|
+
renderMessageLabel={(m) =>
|
|
344
|
+
m.direction === "outgoing" ? "Assistente" : undefined
|
|
345
|
+
}
|
|
346
|
+
/>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Default rendering uses `MessageBubble`, `getDisplayDate` (pt-BR HOJE/ONTEM),
|
|
350
|
+
and pt-BR `HH:mm` timestamps. Default appearance does **not** change corners
|
|
351
|
+
or spacing based on `groupPosition` — that field is for custom renderers.
|
|
352
|
+
|
|
353
|
+
### Custom rich-message renderer
|
|
354
|
+
|
|
355
|
+
Inject `renderMessage` to compose public `Message` + `Bubble` primitives.
|
|
356
|
+
`MessageRenderContext` exposes stable `id`, `direction`, presentation `align`
|
|
357
|
+
(`incoming` → `start`, `outgoing` → `end`), neighbor-derived `groupPosition`
|
|
358
|
+
(`single` | `first` | `middle` | `last`), and optional `label`.
|
|
359
|
+
|
|
360
|
+
Grouping: two adjacent messages share a group when `formatDate(a.sentAt) ===
|
|
361
|
+
formatDate(b.sentAt)` **and** `a.direction === b.direction`. Date boundaries
|
|
362
|
+
and direction changes start a new group.
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import {
|
|
366
|
+
MessageList,
|
|
367
|
+
Message,
|
|
368
|
+
MessageContent,
|
|
369
|
+
MessageFooter,
|
|
370
|
+
Bubble,
|
|
371
|
+
BubbleContent,
|
|
372
|
+
} from "@better-zap/react";
|
|
373
|
+
|
|
374
|
+
<MessageList
|
|
375
|
+
messages={messages}
|
|
376
|
+
renderMessage={({ message, align, groupPosition, label }) => (
|
|
377
|
+
<Message align={align} data-group={groupPosition}>
|
|
378
|
+
<MessageContent>
|
|
379
|
+
{label ? <span>{label}</span> : null}
|
|
380
|
+
<Bubble
|
|
381
|
+
variant={align === "end" ? "primary" : "default"}
|
|
382
|
+
align={align}
|
|
383
|
+
>
|
|
384
|
+
<BubbleContent>{message.content}</BubbleContent>
|
|
385
|
+
</Bubble>
|
|
386
|
+
<MessageFooter>{/* your time / status */}</MessageFooter>
|
|
387
|
+
</MessageContent>
|
|
388
|
+
</Message>
|
|
389
|
+
)}
|
|
390
|
+
/>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Custom date chrome
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
import { MessageList, DateDivider } from "@better-zap/react";
|
|
397
|
+
|
|
398
|
+
<MessageList
|
|
399
|
+
messages={messages}
|
|
400
|
+
formatDate={(iso) => new Date(iso).toLocaleDateString("en-US")}
|
|
401
|
+
renderDateDivider={({ label, date }) => (
|
|
402
|
+
<DateDivider data-raw={date}>{label}</DateDivider>
|
|
403
|
+
)}
|
|
404
|
+
/>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`DateDivider` is a public presentational pill (`children` + standard `div`
|
|
408
|
+
props). Date row ids are `date:${label}` for the first occurrence of a label
|
|
409
|
+
in the list walk; later duplicates use `date:${label}:${occurrence}` (1-based
|
|
410
|
+
after the first) so malformed timestamps that both format to
|
|
411
|
+
`"Invalid Date"` stay unique. Occurrence suffixes may shift when history
|
|
412
|
+
prepends *duplicate-label* groups (accepted).
|
|
413
|
+
|
|
414
|
+
### Standalone scroll props
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
<MessageList
|
|
418
|
+
messages={messages}
|
|
419
|
+
autoScroll={false}
|
|
420
|
+
onScrollTop={() => loadOlder()}
|
|
421
|
+
/>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Also available: `formatTime(iso)` for the **default** bubble timestamp only
|
|
425
|
+
(custom `renderMessage` owns its own time formatting).
|
|
426
|
+
|
|
427
|
+
## MessageView
|
|
428
|
+
|
|
429
|
+
**MessageView** is the chat pane shell (background, empty state, header, content).
|
|
430
|
+
It works **standalone** or inside `WhatsappDashboard`. Leaves use
|
|
431
|
+
`useOptionalWhatsappDashboard()` — no provider required.
|
|
432
|
+
|
|
433
|
+
### Standalone
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
// No provider — always visible (desktop semantics)
|
|
437
|
+
<MessageView>
|
|
438
|
+
<MessageViewHeader conversation={active} onInfoClick={openInfo} />
|
|
439
|
+
<MessageViewContent>
|
|
440
|
+
<MessageList messages={messages} />
|
|
441
|
+
</MessageViewContent>
|
|
442
|
+
</MessageView>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Outside a provider, empty `MessageView` (no children) renders the default
|
|
446
|
+
pt-BR "Better Zap" empty state. Inside a mobile dashboard with no children it
|
|
447
|
+
returns `null` so the list can fill the viewport.
|
|
448
|
+
|
|
449
|
+
### Header composition
|
|
450
|
+
|
|
451
|
+
- `conversation` is optional; omit or pass `children` to replace the identity
|
|
452
|
+
block (name + phone).
|
|
453
|
+
- Back button: `showBackButton ?? ctx?.isMobile ?? false`. Click calls
|
|
454
|
+
`ctx?.setMobileView("list")` then `onBack`.
|
|
455
|
+
- Info button renders **only** when `onInfoClick` is provided (no inert
|
|
456
|
+
controls). `actions` replaces the default info slot entirely.
|
|
457
|
+
- `labels` partial overrides for back/info `aria-label` defaults
|
|
458
|
+
(`"Voltar"` / `"Informações"`).
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
<MessageViewHeader
|
|
462
|
+
conversation={active}
|
|
463
|
+
showBackButton
|
|
464
|
+
onBack={() => setView("list")}
|
|
465
|
+
onInfoClick={openInfo}
|
|
466
|
+
labels={{ back: "Back", info: "Info" }}
|
|
467
|
+
actions={<button type="button">More</button>}
|
|
468
|
+
/>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Empty content seam
|
|
472
|
+
|
|
473
|
+
`MessageViewEmpty` accepts optional `children` to replace the default icon +
|
|
474
|
+
copy. Used automatically by empty `MessageView` on desktop / standalone.
|
|
475
|
+
|
|
476
|
+
### Style merge
|
|
477
|
+
|
|
478
|
+
Consumer `style` composes with internal visibility. When the pane is hidden on
|
|
479
|
+
mobile (`mobileView !== "chat"`), internal `display: "none"` is applied **after**
|
|
480
|
+
consumer styles so a consumer `display` cannot reveal a hidden pane.
|
|
481
|
+
|
|
482
|
+
## WhatsappDashboard
|
|
483
|
+
|
|
484
|
+
Layout provider for list/chat mobile navigation. Context value is memoized.
|
|
485
|
+
|
|
486
|
+
```tsx
|
|
487
|
+
// Uncontrolled
|
|
488
|
+
<WhatsappDashboard defaultMobileView="list">
|
|
489
|
+
<ConversationList ... />
|
|
490
|
+
<MessageView>...</MessageView>
|
|
491
|
+
</WhatsappDashboard>
|
|
492
|
+
|
|
493
|
+
// Controlled navigation + app-owned breakpoint
|
|
494
|
+
const [mobileView, setMobileView] = useState<"list" | "chat">("list");
|
|
495
|
+
|
|
496
|
+
<WhatsappDashboard
|
|
497
|
+
isMobile={isNarrow}
|
|
498
|
+
mobileView={mobileView}
|
|
499
|
+
onMobileViewChange={setMobileView}
|
|
500
|
+
>
|
|
501
|
+
...
|
|
502
|
+
</WhatsappDashboard>
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
- Controlled when `mobileView !== undefined` (local state not written).
|
|
506
|
+
- Uncontrolled uses `defaultMobileView` (`"list"`). `onMobileViewChange` still
|
|
507
|
+
fires when the view changes if provided.
|
|
508
|
+
- When `isMobile` is set, `matchMedia` is not attached; the prop value is used.
|
|
509
|
+
|
|
510
|
+
## Migration from the monolithic API
|
|
511
|
+
|
|
512
|
+
All high-level components you may already be using are unchanged in name and
|
|
513
|
+
remain public — nothing described below removes an export.
|
|
514
|
+
|
|
515
|
+
| Legacy usage | Now | Notes |
|
|
516
|
+
| --- | --- | --- |
|
|
517
|
+
| `MessageBubble` | retained — compatibility adapter over `Message` + `Bubble` | compose primitives directly for custom metadata placement or grouping (see [Message vs Bubble](#message-vs-bubble)) |
|
|
518
|
+
| `MessageInput` | retained — failure-safe adapter over `Composer*` + `useFreeformMessageWindow` | inert action buttons (emoji/attach/mic) no longer render without callbacks (see [Actions and failures](#actions-and-failures)) |
|
|
519
|
+
| `MessageList` | retained — now extensible (`renderMessage`, `renderDateDivider`, grouping context, `formatDate`/`formatTime`) | default rendering is unchanged if you pass no new props |
|
|
520
|
+
| `ConversationList` | retained — standalone-capable, controlled search/filter, `renderItem`/`renderAvatar`/`labels` | no provider required (see [Standalone vs dashboard](#standalone-vs-dashboard)) |
|
|
521
|
+
| `MessageView` (+ header/content/empty) | retained — context-optional | standalone renders desktop semantics, no provider required (see [MessageView](#messageview)) |
|
|
522
|
+
| `WhatsappDashboard` | retained — controllable (`mobileView`/`onMobileViewChange`/`isMobile`) | uncontrolled default behavior is unchanged |
|
|
523
|
+
|
|
524
|
+
### Versioning
|
|
525
|
+
|
|
526
|
+
`@better-zap/react` is `0.x`; per SemVer, minor releases in this range may
|
|
527
|
+
include breaking changes. This wave, however, removes **no** high-level
|
|
528
|
+
export: `MessageBubble`, `MessageInput`, `MessageList`, `ConversationList`,
|
|
529
|
+
`MessageView`, and `WhatsappDashboard` all remain public. The composable
|
|
530
|
+
primitives (`Bubble*`, `Message*` layout parts, `Composer*`, `DateDivider`)
|
|
531
|
+
are additive — they sit alongside the adapters, not in place of them.
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-zap/react",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "React components for Better Zap.",
|
|
5
5
|
"license": "ISC",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Dosbodoke/better-zap",
|
|
9
|
+
"directory": "packages/react"
|
|
10
|
+
},
|
|
6
11
|
"type": "module",
|
|
7
12
|
"main": "./dist/index.cjs",
|
|
8
13
|
"module": "./dist/index.mjs",
|
|
@@ -18,6 +23,51 @@
|
|
|
18
23
|
"import": "./dist/index.mjs",
|
|
19
24
|
"require": "./dist/index.cjs"
|
|
20
25
|
},
|
|
26
|
+
"./bubble": {
|
|
27
|
+
"types": "./dist/bubble.d.mts",
|
|
28
|
+
"import": "./dist/bubble.mjs",
|
|
29
|
+
"require": "./dist/bubble.cjs"
|
|
30
|
+
},
|
|
31
|
+
"./message": {
|
|
32
|
+
"types": "./dist/message.d.mts",
|
|
33
|
+
"import": "./dist/message.mjs",
|
|
34
|
+
"require": "./dist/message.cjs"
|
|
35
|
+
},
|
|
36
|
+
"./message-bubble": {
|
|
37
|
+
"types": "./dist/message-bubble.d.mts",
|
|
38
|
+
"import": "./dist/message-bubble.mjs",
|
|
39
|
+
"require": "./dist/message-bubble.cjs"
|
|
40
|
+
},
|
|
41
|
+
"./composer": {
|
|
42
|
+
"types": "./dist/composer.d.mts",
|
|
43
|
+
"import": "./dist/composer.mjs",
|
|
44
|
+
"require": "./dist/composer.cjs"
|
|
45
|
+
},
|
|
46
|
+
"./message-input": {
|
|
47
|
+
"types": "./dist/message-input.d.mts",
|
|
48
|
+
"import": "./dist/message-input.mjs",
|
|
49
|
+
"require": "./dist/message-input.cjs"
|
|
50
|
+
},
|
|
51
|
+
"./message-view": {
|
|
52
|
+
"types": "./dist/message-view.d.mts",
|
|
53
|
+
"import": "./dist/message-view.mjs",
|
|
54
|
+
"require": "./dist/message-view.cjs"
|
|
55
|
+
},
|
|
56
|
+
"./conversation-list": {
|
|
57
|
+
"types": "./dist/conversation-list.d.mts",
|
|
58
|
+
"import": "./dist/conversation-list.mjs",
|
|
59
|
+
"require": "./dist/conversation-list.cjs"
|
|
60
|
+
},
|
|
61
|
+
"./whatsapp-dashboard": {
|
|
62
|
+
"types": "./dist/whatsapp-dashboard.d.mts",
|
|
63
|
+
"import": "./dist/whatsapp-dashboard.mjs",
|
|
64
|
+
"require": "./dist/whatsapp-dashboard.cjs"
|
|
65
|
+
},
|
|
66
|
+
"./utils": {
|
|
67
|
+
"types": "./dist/utils.d.mts",
|
|
68
|
+
"import": "./dist/utils.mjs",
|
|
69
|
+
"require": "./dist/utils.cjs"
|
|
70
|
+
},
|
|
21
71
|
"./tailwind.css": "./dist/tailwind.css",
|
|
22
72
|
"./package.json": "./package.json"
|
|
23
73
|
},
|
|
@@ -37,15 +87,18 @@
|
|
|
37
87
|
"class-variance-authority": "^0.7.1",
|
|
38
88
|
"clsx": "^2.1.1",
|
|
39
89
|
"tailwind-merge": "^3.0.0",
|
|
40
|
-
"better-zap": "0.
|
|
90
|
+
"better-zap": "0.2.0"
|
|
41
91
|
},
|
|
42
92
|
"peerDependencies": {
|
|
43
93
|
"react": "^19.0.0",
|
|
44
94
|
"react-dom": "^19.0.0"
|
|
45
95
|
},
|
|
46
96
|
"devDependencies": {
|
|
97
|
+
"@testing-library/react": "^16.3.2",
|
|
98
|
+
"@testing-library/user-event": "^14.6.1",
|
|
47
99
|
"@types/react": "^19.0.0",
|
|
48
100
|
"@types/react-dom": "^19.0.0",
|
|
101
|
+
"jsdom": "^29.1.1",
|
|
49
102
|
"tsdown": "^0.21.2",
|
|
50
103
|
"typescript": "^6.0.2",
|
|
51
104
|
"vitest": "^4.1.0"
|
|
@@ -53,6 +106,6 @@
|
|
|
53
106
|
"scripts": {
|
|
54
107
|
"build": "tsdown",
|
|
55
108
|
"typecheck": "tsc --noEmit",
|
|
56
|
-
"test": "vitest run
|
|
109
|
+
"test": "pnpm run build && vitest run"
|
|
57
110
|
}
|
|
58
111
|
}
|