@polymorfa/react 0.1.0-dev.20260922094209
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/LICENSE +5 -0
- package/README.md +303 -0
- package/dist/call-button.d.ts +29 -0
- package/dist/call-button.d.ts.map +1 -0
- package/dist/call-button.js +54 -0
- package/dist/calls.d.ts +201 -0
- package/dist/calls.d.ts.map +1 -0
- package/dist/calls.js +944 -0
- package/dist/components.d.ts +109 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +843 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +82 -0
- package/dist/dropin.d.ts +100 -0
- package/dist/dropin.d.ts.map +1 -0
- package/dist/dropin.js +298 -0
- package/dist/hooks.d.ts +30 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +71 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Polymorfa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software.
|
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# @polymorfa/react
|
|
2
|
+
|
|
3
|
+
React bindings for the framework-neutral controllers in `@polymorfa/browser`.
|
|
4
|
+
|
|
5
|
+
## Drop-in components
|
|
6
|
+
|
|
7
|
+
One provider, one server route, one component:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
"use client";
|
|
11
|
+
import { Inbox, PolymorfaProvider } from "@polymorfa/react";
|
|
12
|
+
|
|
13
|
+
<PolymorfaProvider tokenEndpoint="/api/polymorfa/token">
|
|
14
|
+
<Inbox />
|
|
15
|
+
</PolymorfaProvider>;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
With `tokenEndpoint`, the provider POSTs to your `createPolymorfaHandler`
|
|
19
|
+
route, refreshes the token 60 s before it expires (80 % of the lifetime for
|
|
20
|
+
short tokens), and retries failures with exponential backoff and jitter
|
|
21
|
+
(1 s up to 30 s). A `401` or `403` stops retries until `refresh()`.
|
|
22
|
+
`usePolymorfaClient()` returns the shared client; `usePermissions()` reports
|
|
23
|
+
the grant for rendering only. `appearance` and `locale` work as before.
|
|
24
|
+
|
|
25
|
+
| Component | Needs | Notes |
|
|
26
|
+
| ---------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
+
| `<Inbox/>` | `read_messages` | List, chat and contact panel. Composer needs `send_message`, contact panel `read_contact`, the call button `voip_place` |
|
|
28
|
+
| `<ConversationList/>`, `<ContactPanel/>` | | Inbox parts, usable alone |
|
|
29
|
+
| `<ConnectWhatsAppButton/>` | `connect_whatsapp` | Opens Polymorfa's hosted QuickLink page; never embeds it |
|
|
30
|
+
| `<SessionStatus/>` | | Connection badge with a Retry button |
|
|
31
|
+
| `<CallButton to/>` | `voip_place` | Places a call with the browser Calls client and shows `CallSurface` |
|
|
32
|
+
| `<TemplateManager/>` | `manage_templates` | Template list plus `<TemplateBuilder/>` |
|
|
33
|
+
|
|
34
|
+
When a permission is missing, the control is hidden and development builds
|
|
35
|
+
log one warning, for example
|
|
36
|
+
`<Inbox/>: composer hidden; the token lacks "send_message". Add it to allow in mint().`
|
|
37
|
+
Production builds log nothing.
|
|
38
|
+
|
|
39
|
+
`<Inbox source={...}/>` accepts any `InboxDataSource`. The default reads your
|
|
40
|
+
handler's `history` and `events` routes and sends with the client token. For
|
|
41
|
+
a local cache, pass `createStoreInboxSource(store, { backend })` from
|
|
42
|
+
`@polymorfa/store`. Every part takes `className` and `classNames` for its
|
|
43
|
+
slots and follows `appearance`, including `unstyled`.
|
|
44
|
+
|
|
45
|
+
`<CampaignComposer/>` is not included: campaign creation needs a server
|
|
46
|
+
credential, so it belongs behind a handler route that does not exist yet.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { MessageList, PolymorfaProvider } from "@polymorfa/react";
|
|
50
|
+
|
|
51
|
+
<PolymorfaProvider appearance={{ theme: "dark" }}>
|
|
52
|
+
<MessageList controller={conversationController} />
|
|
53
|
+
</PolymorfaProvider>;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`MessageList`, `ComposeBox`, `ChatDrawer`, `TemplateBuilder`, and
|
|
57
|
+
`CallSurface` subscribe with `useSyncExternalStore`. Controllers may be passed
|
|
58
|
+
directly or created through a component factory; only factory-created
|
|
59
|
+
controllers are disposed by the binding. `useController` and
|
|
60
|
+
`useResolvedController` support custom rendering without forking state logic.
|
|
61
|
+
|
|
62
|
+
The chat and template components add the shared `@polymorfa/ui` stylesheet
|
|
63
|
+
to the document on mount and use `pmfa-*` class names, so a host stylesheet
|
|
64
|
+
can adjust them. Visible text comes from the provider's locale.
|
|
65
|
+
|
|
66
|
+
## Chat
|
|
67
|
+
|
|
68
|
+
`ChatDrawer` shows a conversation with its composer. Pass
|
|
69
|
+
`composerController` (or `createComposerController`) to render the built-in
|
|
70
|
+
`ComposeBox`; each message then offers Reply, which sets the composer's
|
|
71
|
+
reply target. Use `createConversationComposerActions()` from
|
|
72
|
+
`@polymorfa/browser` so the composer sends through the same conversation the
|
|
73
|
+
drawer shows.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import {
|
|
77
|
+
ConversationController,
|
|
78
|
+
MessageComposerController,
|
|
79
|
+
createConversationComposerActions,
|
|
80
|
+
} from "@polymorfa/browser";
|
|
81
|
+
import { ChatDrawer } from "@polymorfa/react";
|
|
82
|
+
|
|
83
|
+
const conversation = new ConversationController(source);
|
|
84
|
+
const composer = new MessageComposerController(
|
|
85
|
+
createConversationComposerActions(conversation, uploadFile),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
<ChatDrawer
|
|
89
|
+
open={open}
|
|
90
|
+
onClose={() => setOpen(false)}
|
|
91
|
+
conversation={conversation}
|
|
92
|
+
composerController={composer}
|
|
93
|
+
title="Casey Rivera"
|
|
94
|
+
/>;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`uploadFile(attachment, onProgress, signal)` reads `attachment.file` and
|
|
98
|
+
resolves with a `MessageAttachment`; set its `url` (and `previewUrl` for
|
|
99
|
+
images) so the message list can show it.
|
|
100
|
+
|
|
101
|
+
- **Messages.** `MessageList` groups consecutive messages by side, adds
|
|
102
|
+
"Today", "Yesterday", or a localized date between days, and shows a
|
|
103
|
+
pending, sent, or failed icon on outbound messages. Images render as lazy
|
|
104
|
+
thumbnails; other files render as a card with name, size, and a link when
|
|
105
|
+
`url` is set. Only `https:`, `http:`, and `blob:` URLs become links or
|
|
106
|
+
images; any other scheme renders the card without one. A reply shows the quoted message, and selecting the quote
|
|
107
|
+
scrolls to and focuses it. Failed outbound messages offer Retry, which
|
|
108
|
+
calls `controller.retry(clientId)`. Pass `onReply` to offer Reply on every
|
|
109
|
+
message.
|
|
110
|
+
- **Composer.** `ComposeBox` attaches files from the paperclip button, a
|
|
111
|
+
paste, or a drop onto the composer. Pending files show upload progress,
|
|
112
|
+
a failed state, and a remove button. A rejected file, such as one over the
|
|
113
|
+
size limit, shows as the composer error. When a reply target is set, a
|
|
114
|
+
banner shows the quoted text (pass `conversation` or `messages` to resolve
|
|
115
|
+
it) with a cancel button. `accept` and `multiple` (default `true`) apply to
|
|
116
|
+
the file picker. Enter sends; Shift+Enter adds a line.
|
|
117
|
+
- **Composer toolbar.** The toolbar reads, in order: `startActions`, the
|
|
118
|
+
emoji button, the attach button, the message field, `endActions`, and
|
|
119
|
+
either the microphone or the send button. The send button replaces the
|
|
120
|
+
microphone once there is text or an uploaded attachment. `placeholder`
|
|
121
|
+
replaces the localized placeholder, and the field grows to `maxRows`
|
|
122
|
+
(default 8) before it scrolls. Every button is at least 40px.
|
|
123
|
+
- **Emoji.** The smiley button (`emoji`, default `true`) opens a picker with
|
|
124
|
+
search, a Recent tab, and nine categories. Arrow keys move through the
|
|
125
|
+
grid, Escape closes it and returns focus to the button, and a picked emoji
|
|
126
|
+
is inserted at the caret. Recent emoji are kept in `localStorage` when it
|
|
127
|
+
is available. The picker flips above or below the button to stay on
|
|
128
|
+
screen, and shows as a bottom sheet at 600px wide or less. The emoji set
|
|
129
|
+
is built in and needs no network.
|
|
130
|
+
- **Voice notes.** Where the browser has `MediaRecorder` and
|
|
131
|
+
`getUserMedia`, an empty composer shows a microphone (`voiceNotes`,
|
|
132
|
+
default `true`). While recording, a bar shows the elapsed time, a live
|
|
133
|
+
input level, a delete button, and a send button. Sending adds the
|
|
134
|
+
recording as an `audio/webm` (`audio/ogg` or `audio/mp4`) attachment through
|
|
135
|
+
`upload`, then sends it; pass `voiceNoteAutoSend={false}` to leave it in
|
|
136
|
+
the composer instead. A denied microphone shows a localized error.
|
|
137
|
+
Recording start and stop are announced politely, and the microphone is
|
|
138
|
+
released when the recording ends or the composer unmounts.
|
|
139
|
+
- **Quick replies.** Pass `quickReplies` (`{ id, shortcut, text,
|
|
140
|
+
description? }[]`). Typing `/` at the start of a word opens a list
|
|
141
|
+
filtered by shortcut and text. The field follows the ARIA combobox
|
|
142
|
+
pattern: Up and Down move, Enter or Tab inserts, and Escape closes the
|
|
143
|
+
list. Choosing an option replaces the `/word` with its `text`, then calls
|
|
144
|
+
`onQuickReply(option)`.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<ComposeBox
|
|
148
|
+
controller={composer}
|
|
149
|
+
placeholder="Reply to Casey"
|
|
150
|
+
quickReplies={[
|
|
151
|
+
{ id: "thanks", shortcut: "thanks", text: "Thanks for reaching out!" },
|
|
152
|
+
]}
|
|
153
|
+
endActions={<TemplateButton />}
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- **Drawer.** The drawer is a non-modal dialog labelled by its title. When
|
|
158
|
+
`open` changes to `true`, it focuses the message field (or the close
|
|
159
|
+
button); closing it returns focus to the element that had it. A drawer
|
|
160
|
+
that mounts open leaves focus alone unless you pass `autoFocus`. Escape
|
|
161
|
+
pressed inside the drawer calls `onClose`, except while an input method is
|
|
162
|
+
composing text. A `composer`
|
|
163
|
+
node replaces the built-in composer, and `conversation` is an alias of
|
|
164
|
+
`controller`.
|
|
165
|
+
- **Custom rendering.** `renderMessage(message)` replaces a bubble's
|
|
166
|
+
content, and `renderAttachment(attachment, message)` replaces one
|
|
167
|
+
attachment.
|
|
168
|
+
|
|
169
|
+
## Styling
|
|
170
|
+
|
|
171
|
+
Every chat and template component takes `className` for its root and
|
|
172
|
+
`classNames` for its inner slots. Each slotted node also carries
|
|
173
|
+
`data-slot`, and `appearance.elements[slot]` applies a class and inline
|
|
174
|
+
styles from the provider:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<PolymorfaProvider
|
|
178
|
+
appearance={{
|
|
179
|
+
theme: "system",
|
|
180
|
+
variables: { colorPrimary: "#0f766e" },
|
|
181
|
+
darkVariables: { colorPrimary: "#14b8a6" },
|
|
182
|
+
elements: { bubble: { styles: { borderRadius: "6px" } } },
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<MessageList
|
|
186
|
+
controller={conversation}
|
|
187
|
+
classNames={{ messageList: "my-log", dateSeparator: "my-date" }}
|
|
188
|
+
/>
|
|
189
|
+
</PolymorfaProvider>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The bundled rules sit in `@layer polymorfa`, so unlayered app CSS overrides
|
|
193
|
+
them. `appearance.unstyled` stops the chat, template, and call components
|
|
194
|
+
from adding their stylesheets. The bundled stylesheet is document-wide and stays
|
|
195
|
+
installed once any styled component has mounted, so its `.pmfa-*` rules still
|
|
196
|
+
match unstyled components rendered alongside styled ones, or after a provider
|
|
197
|
+
switches to `unstyled: true`. Keep unstyled components on a page without styled
|
|
198
|
+
Polymorfa components, or override the bundled rules from unlayered CSS. See the
|
|
199
|
+
[`@polymorfa/ui` styling reference](../ui/README.md#styling) for the variable
|
|
200
|
+
list, dark colors, and every slot name.
|
|
201
|
+
|
|
202
|
+
`TemplateBuilder` edits canonical headers, bodies, footers, button labels,
|
|
203
|
+
carousel card bodies, and variable examples. It keeps “Save draft” and “Submit
|
|
204
|
+
to Meta” as separate actions and renders structured previews. Use
|
|
205
|
+
`renderPreview` to replace the preview region without replacing controller
|
|
206
|
+
state.
|
|
207
|
+
|
|
208
|
+
## Calls
|
|
209
|
+
|
|
210
|
+
`CallSurface` is the whole call experience: the incoming card while a call
|
|
211
|
+
rings, the stage, control dock and participant list during the call, and a ↗
|
|
212
|
+
button that pops the call into its own resizable window. `IncomingCallCard`,
|
|
213
|
+
`CallStage`, `ParticipantVideoGrid`, `ParticipantList`, `CallControls`, and
|
|
214
|
+
`DialPad` compose individually. All of them render from a `CallsController`
|
|
215
|
+
and follow the provider's appearance and locale.
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import {
|
|
219
|
+
createBrowserCalls,
|
|
220
|
+
createClientTokenProvider,
|
|
221
|
+
} from "@polymorfa/browser";
|
|
222
|
+
import { CallSurface, DialPad, PolymorfaProvider } from "@polymorfa/react";
|
|
223
|
+
|
|
224
|
+
// Create once in the browser; call dispose() when the app releases it.
|
|
225
|
+
const calls = createBrowserCalls({
|
|
226
|
+
session: "support",
|
|
227
|
+
getClientToken: createClientTokenProvider(),
|
|
228
|
+
});
|
|
229
|
+
await calls.connect();
|
|
230
|
+
|
|
231
|
+
<PolymorfaProvider>
|
|
232
|
+
<CallSurface
|
|
233
|
+
controller={calls.controller}
|
|
234
|
+
resolveName={lookupContactName}
|
|
235
|
+
exclusive={false}
|
|
236
|
+
/>
|
|
237
|
+
<DialPad controller={calls.controller} />
|
|
238
|
+
</PolymorfaProvider>;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The client token comes from your server (`POST /platform/client-tokens`) and
|
|
242
|
+
needs `voip_place`, `voip_answer` and `voip_signal`. The components use it
|
|
243
|
+
directly; there is no calling ticket. `calls.controller.call` is the shared
|
|
244
|
+
`Call` model. See the [browser package](../browser/README.md#calls) for token
|
|
245
|
+
replacement, lifecycle ownership and limits.
|
|
246
|
+
|
|
247
|
+
### Answering and claims
|
|
248
|
+
|
|
249
|
+
`exclusive` on `CallSurface` and `IncomingCallCard` decides what Answer does:
|
|
250
|
+
|
|
251
|
+
- `false` (default): the call is answered without a claim. Other participants
|
|
252
|
+
keep ringing and can join it.
|
|
253
|
+
- `true`: Answer claims the call. Other participants stop ringing and cannot
|
|
254
|
+
join.
|
|
255
|
+
|
|
256
|
+
The card never declines a call on its own. A call another participant
|
|
257
|
+
answered without a claim shows Join and Dismiss. A call another participant
|
|
258
|
+
claimed shows "Answered by another participant" and only Dismiss, because
|
|
259
|
+
declining would end the call for the participant who answered. Other waiting
|
|
260
|
+
calls are listed under the card with a Show button.
|
|
261
|
+
|
|
262
|
+
In a call, `CallControls` shows Leave on calls nobody claimed (`showLeave`
|
|
263
|
+
overrides this). Leave closes only this browser's connection; the red button
|
|
264
|
+
ends the call for everyone and is labelled "End call for everyone" when Leave
|
|
265
|
+
is shown. A call this browser placed shows only Hang up until it connects,
|
|
266
|
+
because leaving it would keep the callee ringing.
|
|
267
|
+
|
|
268
|
+
### Participant video
|
|
269
|
+
|
|
270
|
+
`CallStage` renders `ParticipantVideoGrid` when anyone sends video: one tile
|
|
271
|
+
per remote participant, keyed by participant or connection so a tile survives
|
|
272
|
+
camera and source changes. Tiles are muted video elements with an accessible
|
|
273
|
+
name ("Video from …") and a visible label; call audio plays through one
|
|
274
|
+
hidden element and follows the selected speaker. Tiles for other app
|
|
275
|
+
connections show their participant reference (`client:<id>` or
|
|
276
|
+
`server:<name>`) unless `labelVideo` names them. `renderMedia` receives `{ local, remote, videos }` to
|
|
277
|
+
replace the media region. `ParticipantList` shows the WhatsApp participants
|
|
278
|
+
and their state.
|
|
279
|
+
|
|
280
|
+
The incoming card mirrors the official desktop call window: name, "WhatsApp
|
|
281
|
+
audio/video call" subtitle, avatar or a mirrored self-preview with camera and
|
|
282
|
+
mute toggles and a ⋯ device menu, then Reject and Answer. Answering a video
|
|
283
|
+
offer with the camera off still acquires video muted, so the in-call camera
|
|
284
|
+
toggle can enable it. The dock carries split pills — `[camera|⌄]` and
|
|
285
|
+
`[mic|⌄]` whose dropdowns pick devices live — the Leave button when shown,
|
|
286
|
+
and a red end-call pill. Video is
|
|
287
|
+
per direction; on an audio call the camera button upgrades to video
|
|
288
|
+
(`controller.enableVideo()`, a re-offer on the same connection) and
|
|
289
|
+
`disableVideo` hides it. A dropped connection shows "Reconnecting…" while the
|
|
290
|
+
controller reconnects media, and gives up as `connection_failed` after the
|
|
291
|
+
resumption window. Avatars come from
|
|
292
|
+
`resolveAvatar` (a URL or a promise of one; `BrowserMessagingClient.contacts
|
|
293
|
+
.picture` works when the token carries `read_contact`); without one a stable
|
|
294
|
+
hash of the caller id picks one of eight palette tones.
|
|
295
|
+
|
|
296
|
+
If a reject or hangup request fails, the call stays active and its controls remain
|
|
297
|
+
available for retry. The UI shows a localized failure message and keeps existing
|
|
298
|
+
media connected until the call ends. `snapshot.error` clears when ending succeeds.
|
|
299
|
+
|
|
300
|
+
While an answer is in flight, the incoming card disables Answer, Join, Reject
|
|
301
|
+
and its camera and microphone choices. A failed answer shows a notice on the
|
|
302
|
+
incoming card: on the call itself when it is still ringing, or on the next
|
|
303
|
+
waiting call when the answered call's media failed.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type BrowserCalls, type PolymorfaClient } from "@polymorfa/browser";
|
|
2
|
+
import type { SlotClassNames } from "@polymorfa/ui";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* The provider's shared Calls client for a session, created on first use
|
|
6
|
+
* with the provider's client token.
|
|
7
|
+
*/
|
|
8
|
+
export declare function browserCallsFor(client: PolymorfaClient, session: string): BrowserCalls;
|
|
9
|
+
export interface CallButtonProps {
|
|
10
|
+
/** Phone number in E.164 form or a WhatsApp user ID. */
|
|
11
|
+
readonly to: string;
|
|
12
|
+
readonly video?: boolean;
|
|
13
|
+
/** Session that places the call. Defaults to the grant's session. */
|
|
14
|
+
readonly session?: string;
|
|
15
|
+
/** `icon` renders a round icon button, as in the inbox header. */
|
|
16
|
+
readonly variant?: "button" | "icon";
|
|
17
|
+
/** Render the call overlay after placing. Defaults to `true`. */
|
|
18
|
+
readonly surface?: boolean;
|
|
19
|
+
readonly children?: ReactNode;
|
|
20
|
+
readonly onError?: (error: unknown) => void;
|
|
21
|
+
readonly className?: string;
|
|
22
|
+
readonly classNames?: SlotClassNames;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Places a WhatsApp call with the Calls client. Hidden unless the token
|
|
26
|
+
* allows `voip_place`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function CallButton({ to, video, session, variant, surface, children, onError, className, classNames, }: CallButtonProps): import("react").JSX.Element | null;
|
|
29
|
+
//# sourceMappingURL=call-button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call-button.d.ts","sourceRoot":"","sources":["../src/call-button.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAOjD;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,MAAM,GACd,YAAY,CAgBd;AAED,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,iEAAiE;IACjE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC;CACtC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EACzB,EAAE,EACF,KAAa,EACb,OAAO,EACP,OAAkB,EAClB,OAAc,EACd,QAAQ,EACR,OAAO,EACP,SAAS,EACT,UAAU,GACX,EAAE,eAAe,sCA2CjB"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { createBrowserCalls, } from "@polymorfa/browser";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { CallSurface } from "./calls.js";
|
|
5
|
+
import { Icon, text, useShell, useSlots } from "./components.js";
|
|
6
|
+
import { usePermission, usePolymorfa, usePolymorfaClient } from "./context.js";
|
|
7
|
+
const shared = new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* The provider's shared Calls client for a session, created on first use
|
|
10
|
+
* with the provider's client token.
|
|
11
|
+
*/
|
|
12
|
+
export function browserCallsFor(client, session) {
|
|
13
|
+
let bySession = shared.get(client);
|
|
14
|
+
if (bySession === undefined) {
|
|
15
|
+
bySession = new Map();
|
|
16
|
+
shared.set(client, bySession);
|
|
17
|
+
}
|
|
18
|
+
let calls = bySession.get(session);
|
|
19
|
+
if (calls === undefined) {
|
|
20
|
+
calls = createBrowserCalls({
|
|
21
|
+
session,
|
|
22
|
+
getClientToken: client.getClientToken,
|
|
23
|
+
...(client.baseUrl === undefined ? {} : { baseUrl: client.baseUrl }),
|
|
24
|
+
});
|
|
25
|
+
bySession.set(session, calls);
|
|
26
|
+
}
|
|
27
|
+
return calls;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Places a WhatsApp call with the Calls client. Hidden unless the token
|
|
31
|
+
* allows `voip_place`.
|
|
32
|
+
*/
|
|
33
|
+
export function CallButton({ to, video = false, session, variant = "button", surface = true, children, onError, className, classNames, }) {
|
|
34
|
+
const client = usePolymorfaClient();
|
|
35
|
+
const configuration = usePolymorfa();
|
|
36
|
+
const slots = useSlots(classNames);
|
|
37
|
+
const root = useShell(slots, "callButton", variant === "icon" ? "pmfa-btn pmfa-btn-ghost pmfa-btn-icon" : "pmfa-btn", className);
|
|
38
|
+
const allowed = usePermission("CallButton", "button", "voip_place");
|
|
39
|
+
const [calls, setCalls] = useState();
|
|
40
|
+
const [busy, setBusy] = useState(false);
|
|
41
|
+
const resolvedSession = session ?? client.getSnapshot().grant?.session;
|
|
42
|
+
if (!allowed || resolvedSession === undefined)
|
|
43
|
+
return null;
|
|
44
|
+
const label = text(configuration, video ? "calls.videoCall" : "calls.call");
|
|
45
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", ...root, "data-pmfa": "call-button", "aria-label": variant === "icon" ? label : undefined, title: variant === "icon" ? label : undefined, disabled: busy, onClick: () => {
|
|
46
|
+
const instance = browserCallsFor(client, resolvedSession);
|
|
47
|
+
setCalls(instance);
|
|
48
|
+
setBusy(true);
|
|
49
|
+
(instance.connected ? Promise.resolve() : instance.connect())
|
|
50
|
+
.then(() => instance.controller.place(to, { video }))
|
|
51
|
+
.catch((error) => onError?.(error))
|
|
52
|
+
.finally(() => setBusy(false));
|
|
53
|
+
}, children: [_jsx(Icon, { name: video ? "video" : "phone" }), variant === "icon" ? null : (children ?? label)] }), surface && calls !== undefined && (_jsx(CallSurface, { controller: calls.controller }))] }));
|
|
54
|
+
}
|
package/dist/calls.d.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { type CallParticipant, type CallsController, type CallsSnapshot, type RemoteVideoInfo } from "@polymorfa/browser";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
type ControllerProps = {
|
|
4
|
+
readonly controller?: CallsController;
|
|
5
|
+
readonly createController?: () => CallsController;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Inject the calls stylesheet once per document. SSR-safe (no-op without a
|
|
9
|
+
* `document`) and idempotent.
|
|
10
|
+
*/
|
|
11
|
+
export declare function injectCallsStyles(target?: Document | undefined): void;
|
|
12
|
+
/** Resolve a peer's picture: a URL, nothing, or a promise of either. */
|
|
13
|
+
export type AvatarResolver = (peer: string) => string | undefined | Promise<string | undefined>;
|
|
14
|
+
export interface IncomingCallCardProps extends ControllerProps {
|
|
15
|
+
/** Human name to show instead of the raw number, if the app knows one. */
|
|
16
|
+
readonly displayName?: string;
|
|
17
|
+
readonly avatarUrl?: string;
|
|
18
|
+
readonly resolveAvatar?: AvatarResolver;
|
|
19
|
+
readonly className?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Claim the call when Answer is pressed, so other participants stop
|
|
22
|
+
* ringing and cannot join. Default `false`: others keep ringing and can
|
|
23
|
+
* join the answered call.
|
|
24
|
+
*/
|
|
25
|
+
readonly exclusive?: boolean;
|
|
26
|
+
/** Name for another waiting call in the "Other incoming calls" list. */
|
|
27
|
+
readonly resolveName?: (peer: string) => string | undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The incoming-call window: name, "WhatsApp audio/video call" subtitle, the
|
|
31
|
+
* media middle (avatar for audio; a mirrored self-preview with camera/mic
|
|
32
|
+
* toggles and a ⋯ device menu for video), then Reject / Answer. Answering a
|
|
33
|
+
* video offer with the camera toggled off still acquires video muted, so the
|
|
34
|
+
* in-call camera toggle can enable it later.
|
|
35
|
+
*
|
|
36
|
+
* A call another participant answered without claiming shows Join instead of
|
|
37
|
+
* Answer. A call another participant claimed shows only Dismiss: declining it
|
|
38
|
+
* would end it for the participant who answered. Other waiting calls are
|
|
39
|
+
* listed below; the card never declines them.
|
|
40
|
+
*/
|
|
41
|
+
export declare function IncomingCallCard({ controller, createController, displayName, avatarUrl, resolveAvatar, className, exclusive, resolveName, }: IncomingCallCardProps): import("react").JSX.Element | null;
|
|
42
|
+
/**
|
|
43
|
+
* Seconds since the call connected, ticking once a second (0 before that).
|
|
44
|
+
* A call that drops to `reconnecting` keeps counting: the controller holds
|
|
45
|
+
* `connectedAt` across the flap, and the call it measures is the same one.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useCallDuration(snapshot: Pick<CallsSnapshot, "status" | "connectedAt">): number;
|
|
48
|
+
/** Format seconds as `m:ss` (or `h:mm:ss` past an hour). */
|
|
49
|
+
export declare function formatDuration(totalSeconds: number): string;
|
|
50
|
+
export interface CallStageProps extends ControllerProps {
|
|
51
|
+
readonly displayName?: string;
|
|
52
|
+
readonly avatarUrl?: string;
|
|
53
|
+
readonly resolveAvatar?: AvatarResolver;
|
|
54
|
+
readonly className?: string;
|
|
55
|
+
/** Label for a remote video tile; see {@link ParticipantVideoGridProps}. */
|
|
56
|
+
readonly labelVideo?: ParticipantVideoGridProps["labelVideo"];
|
|
57
|
+
/**
|
|
58
|
+
* Replace the media region without forking state. `remote` is the merged
|
|
59
|
+
* call audio; `videos` holds one stream per remote participant.
|
|
60
|
+
*/
|
|
61
|
+
readonly renderMedia?: (streams: {
|
|
62
|
+
readonly local?: MediaStream;
|
|
63
|
+
readonly remote?: MediaStream;
|
|
64
|
+
readonly videos: readonly {
|
|
65
|
+
readonly key: string;
|
|
66
|
+
readonly info: RemoteVideoInfo;
|
|
67
|
+
readonly stream: MediaStream;
|
|
68
|
+
}[];
|
|
69
|
+
}) => ReactNode;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The in-call stage: one tile per remote participant video when anyone sends
|
|
73
|
+
* video, the centered avatar hero otherwise, the local preview as a
|
|
74
|
+
* picture-in-picture tile, and the identity (name + flashing number/duration
|
|
75
|
+
* line). The merged call audio plays through a hidden element. Always dark by
|
|
76
|
+
* design.
|
|
77
|
+
*/
|
|
78
|
+
export declare function CallStage({ controller, createController, displayName, avatarUrl, resolveAvatar, className, labelVideo, renderMedia, }: CallStageProps): import("react").JSX.Element | null;
|
|
79
|
+
export interface ParticipantVideoGridProps extends ControllerProps {
|
|
80
|
+
readonly className?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Label for a tile. Defaults to the participant's number or ID, then the
|
|
83
|
+
* participant reference of another app connection, then "Participant".
|
|
84
|
+
*/
|
|
85
|
+
readonly labelVideo?: (video: RemoteVideoInfo) => string | undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One tile per remote participant video, keyed by participant or
|
|
89
|
+
* connection so a tile survives source changes. Videos are separate
|
|
90
|
+
* streams; nothing is composed. Renders nothing without remote video.
|
|
91
|
+
*/
|
|
92
|
+
export declare function ParticipantVideoGrid({ controller, createController, className, labelVideo, }: ParticipantVideoGridProps): import("react").JSX.Element | null;
|
|
93
|
+
export interface ParticipantListProps extends ControllerProps {
|
|
94
|
+
readonly className?: string;
|
|
95
|
+
/** Display name for a participant, when the application knows one. */
|
|
96
|
+
readonly resolveName?: (participant: CallParticipant) => string | undefined;
|
|
97
|
+
}
|
|
98
|
+
/** The displayed call's WhatsApp participants and their state. */
|
|
99
|
+
export declare function ParticipantList({ controller, createController, className, resolveName, }: ParticipantListProps): import("react").JSX.Element | null;
|
|
100
|
+
export interface CallControlsProps extends ControllerProps {
|
|
101
|
+
readonly className?: string;
|
|
102
|
+
/** Hide the camera control even when the call can carry video. */
|
|
103
|
+
readonly disableVideo?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Show a Leave button that closes only this connection. Defaults to
|
|
106
|
+
* showing it on calls nobody claimed, where others can stay in the call.
|
|
107
|
+
* Never shown on a call this client placed that has not connected: nobody
|
|
108
|
+
* else is on it, so the red button ends it. The red button always ends the
|
|
109
|
+
* call for everyone.
|
|
110
|
+
*/
|
|
111
|
+
readonly showLeave?: boolean;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The in-call dock, after the official window: the camera split group
|
|
115
|
+
* ([toggle|⌄] — the dropdown picks the camera) and the mic split group
|
|
116
|
+
* ([toggle|⌄] — the dropdown picks microphone + speaker) on the left, the
|
|
117
|
+
* red hang-up pill on the right. The camera group renders whenever the line
|
|
118
|
+
* can carry video (unless `disableVideo`) and is disabled until the call
|
|
119
|
+
* connects. Video is per direction: the camera controls only the outgoing
|
|
120
|
+
* stream.
|
|
121
|
+
*/
|
|
122
|
+
export declare function CallControls({ controller, createController, className, disableVideo, showLeave, }: CallControlsProps): import("react").JSX.Element | null;
|
|
123
|
+
export interface DialPadProps extends ControllerProps {
|
|
124
|
+
/** Prefill the number field. */
|
|
125
|
+
readonly defaultValue?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Offer a video-call button. Default `true`. Set it to `false` for numbers
|
|
128
|
+
* whose calls cannot carry video.
|
|
129
|
+
*/
|
|
130
|
+
readonly allowVideo?: boolean;
|
|
131
|
+
/** Called once the call is actually placed, not when a placement fails. */
|
|
132
|
+
readonly onPlaced?: (to: string) => void;
|
|
133
|
+
readonly className?: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A dial pad card: E.164 entry plus place-call buttons — audio always, and a
|
|
137
|
+
* video-call button unless `allowVideo` is false.
|
|
138
|
+
*/
|
|
139
|
+
export declare function DialPad({ controller, createController, defaultValue, allowVideo, onPlaced, className, }: DialPadProps): import("react").JSX.Element;
|
|
140
|
+
export interface CallPopoutHandle {
|
|
141
|
+
/** True while the separate call window is open. */
|
|
142
|
+
readonly popped: boolean;
|
|
143
|
+
/** Open the call window (no-op if already open or the popup was blocked). */
|
|
144
|
+
readonly open: () => void;
|
|
145
|
+
/** Close the call window (popping the call back inline). */
|
|
146
|
+
readonly close: () => void;
|
|
147
|
+
/** Portal target inside the call window; null while not popped. */
|
|
148
|
+
readonly container: HTMLElement | null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Owns a separate, freely resizable call window (like the desktop client's)
|
|
152
|
+
* and hands back a container to portal the stage + controls into. Media
|
|
153
|
+
* streams keep playing across same-origin windows, so portal'd `<video>`
|
|
154
|
+
* elements just work.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* @param title Window title. `CallSurface` passes the localized
|
|
158
|
+
* `calls.popoutTitle`; the default keeps the hook usable on its own.
|
|
159
|
+
*/
|
|
160
|
+
export declare function useCallPopout(title?: string): CallPopoutHandle;
|
|
161
|
+
export interface CallSurfaceProps extends ControllerProps {
|
|
162
|
+
/** Resolve a display name for a number (contact lookup). */
|
|
163
|
+
readonly resolveName?: (peer: string) => string | undefined;
|
|
164
|
+
/** Resolve an avatar URL for a number (sync or async). */
|
|
165
|
+
readonly resolveAvatar?: AvatarResolver;
|
|
166
|
+
/** Hide the camera control even when the call can carry video. */
|
|
167
|
+
readonly disableVideo?: boolean;
|
|
168
|
+
/** Offer the ↗ pop-out window button. Defaults to true. */
|
|
169
|
+
readonly popout?: boolean;
|
|
170
|
+
/** Replace the stage's media region without forking state. */
|
|
171
|
+
readonly renderMedia?: CallStageProps["renderMedia"];
|
|
172
|
+
/**
|
|
173
|
+
* Claim calls answered from this surface. Default `false`: other
|
|
174
|
+
* participants keep ringing and can join.
|
|
175
|
+
*/
|
|
176
|
+
readonly exclusive?: boolean;
|
|
177
|
+
/** Show the participant list during a call. Defaults to true. */
|
|
178
|
+
readonly showParticipants?: boolean;
|
|
179
|
+
/** Show the Leave button; see {@link CallControlsProps.showLeave}. */
|
|
180
|
+
readonly showLeave?: boolean;
|
|
181
|
+
/** Label for a remote video tile. */
|
|
182
|
+
readonly labelVideo?: ParticipantVideoGridProps["labelVideo"];
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The whole call experience in one component: a fixed overlay that shows the
|
|
186
|
+
* incoming-call card while ringing and the stage + control dock during a
|
|
187
|
+
* call. The ↗ button pops the call into its own resizable browser window;
|
|
188
|
+
* closing that window (or the call ending) pops it back inline.
|
|
189
|
+
*/
|
|
190
|
+
export declare function CallSurface({ controller, createController, resolveName, resolveAvatar, disableVideo, popout: allowPopout, renderMedia, exclusive, showParticipants, showLeave, labelVideo, }: CallSurfaceProps): import("react").JSX.Element | null;
|
|
191
|
+
/**
|
|
192
|
+
* One injected `<style>` tag, scoped under `.pmfa-calls` so nothing leaks into
|
|
193
|
+
* the host app, inside the `polymorfa` cascade layer so unlayered host CSS
|
|
194
|
+
* wins. Colors derive from the shared `--pmfa-*` appearance variables
|
|
195
|
+
* with the monochrome Material-3 palette as fallbacks; the verbs use
|
|
196
|
+
* `--pmfa-color-success` / `--pmfa-color-danger`. All motion is disabled under
|
|
197
|
+
* `prefers-reduced-motion: reduce`.
|
|
198
|
+
*/
|
|
199
|
+
export declare const CALLS_STYLES = "\n@layer polymorfa {\n.pmfa-calls {\n --pmfa-calls-accent: var(--pmfa-color-foreground, oklch(0.15 0 0));\n --pmfa-calls-accent-soft: oklch(0.96 0 0);\n --pmfa-calls-answer: var(--pmfa-color-success, oklch(0.76 0.19 150));\n --pmfa-calls-answer-strong: oklch(0.68 0.18 150);\n --pmfa-calls-danger: var(--pmfa-color-danger, oklch(0.5 0.19 28));\n --pmfa-calls-danger-strong: oklch(0.44 0.18 28);\n --pmfa-calls-card: var(--pmfa-color-background, oklch(1 0 0));\n --pmfa-calls-fg: var(--pmfa-color-foreground, oklch(0.17 0 0));\n --pmfa-calls-muted: var(--pmfa-color-muted, oklch(0.49 0.008 250));\n --pmfa-calls-border: var(--pmfa-color-border, oklch(0.91 0 0));\n --pmfa-calls-stage: oklch(0.15 0 0);\n --pmfa-calls-stage-fg: oklch(0.985 0 0);\n --pmfa-calls-stage-muted: oklch(0.75 0 0);\n --pmfa-calls-stage-line: oklch(1 0 0 / 0.12);\n --pmfa-calls-avatar-1: oklch(0.55 0.09 200);\n --pmfa-calls-avatar-2: oklch(0.55 0.12 255);\n --pmfa-calls-avatar-3: oklch(0.55 0.14 292);\n --pmfa-calls-avatar-4: oklch(0.58 0.14 10);\n --pmfa-calls-avatar-5: oklch(0.6 0.11 70);\n --pmfa-calls-avatar-6: oklch(0.55 0.11 150);\n --pmfa-calls-avatar-7: oklch(0.5 0.13 275);\n --pmfa-calls-avatar-8: oklch(0.55 0.14 330);\n --pmfa-calls-radius: var(--pmfa-radius-medium, 0.5rem);\n --pmfa-calls-font: var(--pmfa-font-family, \"Roboto\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif);\n font-family: var(--pmfa-calls-font);\n color: var(--pmfa-calls-fg);\n font-size: 14px;\n line-height: 1.45;\n -webkit-font-smoothing: antialiased;\n}\n.pmfa-calls *, .pmfa-calls *::before, .pmfa-calls *::after { box-sizing: border-box; }\n.pmfa-calls :focus-visible { outline: 2px solid var(--pmfa-calls-accent); outline-offset: 2px; }\n.pmfa-calls.pmfa-calls-dark {\n --pmfa-calls-accent: oklch(0.92 0 0);\n --pmfa-calls-accent-soft: oklch(1 0 0 / 0.08);\n --pmfa-calls-card: oklch(0.21 0 0);\n --pmfa-calls-fg: oklch(0.985 0 0);\n --pmfa-calls-muted: oklch(0.74 0 0);\n --pmfa-calls-border: oklch(1 0 0 / 0.12);\n}\n@media (prefers-color-scheme: dark) {\n .pmfa-calls.pmfa-calls-auto {\n --pmfa-calls-accent: oklch(0.92 0 0);\n --pmfa-calls-accent-soft: oklch(1 0 0 / 0.08);\n --pmfa-calls-card: oklch(0.21 0 0);\n --pmfa-calls-fg: oklch(0.985 0 0);\n --pmfa-calls-muted: oklch(0.74 0 0);\n --pmfa-calls-border: oklch(1 0 0 / 0.12);\n }\n}\n.pmfa-calls-card { background: var(--pmfa-calls-card); border: 1px solid var(--pmfa-calls-border); border-radius: var(--pmfa-calls-radius); }\n.pmfa-calls-incoming { position: relative; width: 100%; padding: 22px 16px 18px; display: flex; flex-direction: column; text-align: center; background: var(--pmfa-calls-stage); color: var(--pmfa-calls-stage-fg); border-color: var(--pmfa-calls-stage-line); animation: pmfa-calls-rise 0.28s cubic-bezier(0.16, 1, 0.3, 1); }\n.pmfa-calls-incoming .pmfa-calls-name { font-size: 19px; font-weight: 600; }\n.pmfa-calls-subtitle { margin-top: 3px; font-size: 13px; color: var(--pmfa-calls-stage-muted); }\n.pmfa-calls-media { position: relative; width: 100%; margin-top: 16px; border-radius: var(--pmfa-calls-radius); overflow: hidden; aspect-ratio: 16 / 10; background: oklch(0.08 0 0); }\n.pmfa-calls-preview { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1); }\n.pmfa-calls-media-audio { overflow: visible; background: transparent; display: flex; align-items: center; justify-content: center; }\n.pmfa-calls-media-audio .pmfa-calls-avatar-wrap { margin-bottom: 0; }\n.pmfa-calls-mini { width: 36px; height: 36px; background: oklch(1 0 0 / 0.14); color: oklch(0.985 0 0); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }\n.pmfa-calls-mini:hover { background: oklch(1 0 0 / 0.22); }\n.pmfa-calls-mini svg { width: 16px; height: 16px; }\n.pmfa-calls-mini.pmfa-calls-on { background: oklch(0.985 0 0); color: oklch(0.2 0 0); }\n.pmfa-calls-more { position: absolute; top: 10px; right: 10px; z-index: 2; }\n.pmfa-calls-media-ctrls { position: absolute; bottom: 10px; left: 0; right: 0; display: flex; justify-content: center; gap: 10px; z-index: 2; }\n.pmfa-calls-pop-float { position: absolute; left: 50%; transform: translateX(-50%); top: 96px; z-index: 4; }\n.pmfa-calls-avatar-wrap { position: relative; width: 72px; height: 72px; margin-bottom: 14px; }\n.pmfa-calls-avatar { position: relative; z-index: 1; width: 72px; height: 72px; border-radius: 50%; display: flex; align-items: center; justify-content: center; overflow: hidden; color: oklch(0.985 0 0); font-size: 24px; font-weight: 500; letter-spacing: 0.01em; background: oklch(0.22 0 0); }\n.pmfa-calls-audio-hero .pmfa-calls-avatar { border: 1.5px solid var(--pmfa-calls-stage-line); }\n.pmfa-calls-avatar-c1 { background: var(--pmfa-calls-avatar-1); }\n.pmfa-calls-avatar-c2 { background: var(--pmfa-calls-avatar-2); }\n.pmfa-calls-avatar-c3 { background: var(--pmfa-calls-avatar-3); }\n.pmfa-calls-avatar-c4 { background: var(--pmfa-calls-avatar-4); }\n.pmfa-calls-avatar-c5 { background: var(--pmfa-calls-avatar-5); }\n.pmfa-calls-avatar-c6 { background: var(--pmfa-calls-avatar-6); }\n.pmfa-calls-avatar-c7 { background: var(--pmfa-calls-avatar-7); }\n.pmfa-calls-avatar-c8 { background: var(--pmfa-calls-avatar-8); }\n.pmfa-calls-avatar img { width: 100%; height: 100%; object-fit: cover; }\n.pmfa-calls-name { font-size: 16px; font-weight: 500; letter-spacing: 0.01em; text-wrap: balance; }\n.pmfa-calls-peer { font-size: 12.5px; font-variant-numeric: tabular-nums; color: var(--pmfa-calls-muted); letter-spacing: 0.01em; }\n.pmfa-calls-flash { animation: pmfa-calls-swap 0.35s ease; }\n@keyframes pmfa-calls-swap { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }\n.pmfa-calls-btn { appearance: none; border: none; margin: 0; padding: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 50%; color: oklch(0.985 0 0); transition: background-color 0.14s ease; }\n.pmfa-calls-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n.pmfa-calls-btn svg { width: 22px; height: 22px; }\n.pmfa-calls-actions { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-top: 22px; width: 100%; }\n.pmfa-calls-action { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 11.5px; color: var(--pmfa-calls-stage-muted); }\n.pmfa-calls-btn-answer { width: 56px; height: 56px; background: var(--pmfa-calls-answer); animation: pmfa-calls-beckon 2s ease-in-out infinite; }\n.pmfa-calls-btn-answer:hover { background: var(--pmfa-calls-answer-strong); }\n.pmfa-calls-btn-decline { width: 56px; height: 56px; background: var(--pmfa-calls-danger); }\n.pmfa-calls-btn-decline:hover { background: var(--pmfa-calls-danger-strong); }\n@keyframes pmfa-calls-beckon { 0%, 100% { box-shadow: 0 0 0 0 oklch(0.76 0.19 150 / 0.4); } 50% { box-shadow: 0 0 0 10px oklch(0.76 0.19 150 / 0); } }\n@keyframes pmfa-calls-fade { from { opacity: 0; } to { opacity: 1; } }\n.pmfa-calls-pop { width: 236px; padding: 12px; display: flex; flex-direction: column; gap: 10px; text-align: left; background: var(--pmfa-calls-card); color: var(--pmfa-calls-fg); border: 1px solid var(--pmfa-calls-border); border-radius: calc(var(--pmfa-calls-radius) + 4px); animation: pmfa-calls-fade 0.16s ease; }\n.pmfa-calls-field { display: flex; flex-direction: column; gap: 4px; font-size: 10.5px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.07em; color: var(--pmfa-calls-muted); }\n.pmfa-calls-select { width: 100%; padding: 7px 8px; font: inherit; font-family: var(--pmfa-calls-font); font-size: 12.5px; font-weight: 400; text-transform: none; letter-spacing: normal; color: var(--pmfa-calls-fg); background: transparent; border: 1px solid var(--pmfa-calls-border); border-radius: var(--pmfa-calls-radius); }\n.pmfa-calls-select:focus { outline: none; border-color: var(--pmfa-calls-accent); }\n.pmfa-calls-select option { color: initial; background: initial; }\n.pmfa-calls-stage { position: relative; width: 100%; aspect-ratio: 4 / 3; border-radius: var(--pmfa-calls-radius); overflow: hidden; background: radial-gradient(130% 90% at 50% 0%, oklch(1 0 0 / 0.06) 0%, transparent 62%), var(--pmfa-calls-stage); color: var(--pmfa-calls-stage-fg); }\n.pmfa-calls-audio-sink { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }\n.pmfa-calls-grid { position: absolute; inset: 0; margin: 0; padding: 4px; list-style: none; display: grid; gap: 4px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 120px), 1fr)); grid-auto-rows: 1fr; background: var(--pmfa-calls-stage); }\n.pmfa-calls-grid[data-count=\"1\"] { grid-template-columns: 1fr; padding: 0; }\n.pmfa-calls-tile { position: relative; min-height: 0; overflow: hidden; border-radius: var(--pmfa-calls-radius); background: oklch(0.08 0 0); }\n.pmfa-calls-grid[data-count=\"1\"] .pmfa-calls-tile { border-radius: 0; }\n.pmfa-calls-tile video { width: 100%; height: 100%; object-fit: cover; display: block; }\n.pmfa-calls-tile-label { position: absolute; left: 6px; bottom: 6px; max-width: calc(100% - 12px); padding: 2px 6px; border-radius: 4px; font-size: 11.5px; color: var(--pmfa-calls-stage-fg); background: oklch(0.1 0 0 / 0.6); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pmfa-calls-btn-dismiss { width: 56px; height: 56px; background: oklch(1 0 0 / 0.14); }\n.pmfa-calls-btn-dismiss:hover { background: oklch(1 0 0 / 0.22); }\n.pmfa-calls-btn-leave { width: 46px; }\n.pmfa-calls-others, .pmfa-calls-people { width: 100%; margin-top: 14px; text-align: left; }\n.pmfa-calls-people { margin-top: 0; padding: 12px 14px; }\n.pmfa-calls-others ul, .pmfa-calls-people ul { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }\n.pmfa-calls-others li, .pmfa-calls-people li { display: flex; align-items: center; gap: 8px; font-size: 13px; min-width: 0; }\n.pmfa-calls-others li > span:first-child, .pmfa-calls-people li > span:first-child { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.pmfa-calls-others-title { font-size: 10.5px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.07em; color: var(--pmfa-calls-muted); }\n.pmfa-calls-incoming .pmfa-calls-others-title, .pmfa-calls-others-state { color: var(--pmfa-calls-stage-muted); }\n.pmfa-calls-people .pmfa-calls-others-state { color: var(--pmfa-calls-muted); }\n.pmfa-calls-others-state { font-size: 12px; white-space: nowrap; }\n.pmfa-calls-chip { appearance: none; border: 1px solid var(--pmfa-calls-stage-line); background: transparent; color: inherit; font: inherit; font-size: 12px; padding: 4px 10px; border-radius: 999px; cursor: pointer; }\n.pmfa-calls-chip:hover { background: oklch(1 0 0 / 0.12); }\n.pmfa-calls-audio-hero { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; text-align: center; }\n.pmfa-calls-audio-hero .pmfa-calls-name { font-size: 19px; }\n.pmfa-calls-audio-hero .pmfa-calls-peer { color: var(--pmfa-calls-stage-muted); }\n.pmfa-calls-hero-preview { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px; padding: 0 24px; text-align: center; }\n.pmfa-calls-hero-preview .pmfa-calls-peer { color: var(--pmfa-calls-stage-muted); }\n.pmfa-calls-inset { position: relative; width: 82%; aspect-ratio: 16 / 10; border-radius: var(--pmfa-calls-radius); overflow: hidden; background: oklch(0.08 0 0); }\n.pmfa-calls-pip { position: absolute; right: 14px; bottom: 14px; width: 96px; aspect-ratio: 3 / 4; border-radius: var(--pmfa-calls-radius); overflow: hidden; border: 1.5px solid var(--pmfa-calls-stage-line); background: oklch(0.24 0 0); }\n.pmfa-calls-pip video { width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1); }\n.pmfa-calls-stage-id { position: absolute; top: 0; left: 0; right: 0; padding: 16px 18px 30px; background: linear-gradient(oklch(0.1 0 0 / 0.6), transparent); color: var(--pmfa-calls-stage-fg); }\n.pmfa-calls-stage-id .pmfa-calls-peer { color: var(--pmfa-calls-stage-muted); }\n.pmfa-calls-dock-col { position: relative; display: inline-flex; flex-direction: column; align-items: center; }\n.pmfa-calls-dock-col > .pmfa-calls-pop { position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%); z-index: 5; }\n.pmfa-calls-dock { display: inline-flex; align-items: center; gap: 12px; padding: 10px 14px; border-radius: 999px; border: 1px solid var(--pmfa-calls-stage-line); background: oklch(0.17 0 0 / 0.72); backdrop-filter: blur(18px) saturate(1.4); -webkit-backdrop-filter: blur(18px) saturate(1.4); animation: pmfa-calls-rise 0.32s cubic-bezier(0.16, 1, 0.3, 1); }\n.pmfa-calls-btn-ctrl { width: 46px; height: 46px; background: oklch(1 0 0 / 0.1); color: oklch(0.985 0 0); }\n.pmfa-calls-btn-ctrl:hover { background: oklch(1 0 0 / 0.18); }\n.pmfa-calls-group { display: inline-flex; align-items: stretch; border-radius: 999px; background: oklch(1 0 0 / 0.1); overflow: hidden; }\n.pmfa-calls-group.pmfa-calls-disabled { opacity: 0.45; }\n.pmfa-calls-group .pmfa-calls-btn-ctrl { background: transparent; border-radius: 0; }\n.pmfa-calls-group .pmfa-calls-btn-ctrl:hover { background: oklch(1 0 0 / 0.12); }\n.pmfa-calls-group .pmfa-calls-btn-ctrl.pmfa-calls-on { background: oklch(0.985 0 0); }\n.pmfa-calls-btn-acc { width: 30px; height: 46px; border-radius: 0; background: transparent; color: oklch(0.985 0 0 / 0.8); border-left: 1px solid oklch(1 0 0 / 0.12); }\n.pmfa-calls-btn-acc:hover { background: oklch(1 0 0 / 0.12); transform: none; }\n.pmfa-calls-btn-acc svg { width: 14px; height: 14px; transition: transform 0.18s ease; }\n.pmfa-calls-btn-acc.pmfa-calls-open svg { transform: rotate(180deg); }\n.pmfa-calls-btn-ctrl.pmfa-calls-on { background: oklch(0.985 0 0); color: oklch(0.2 0 0); }\n.pmfa-calls-btn-hangup { width: 64px; height: 46px; border-radius: 999px; background: var(--pmfa-calls-danger); }\n.pmfa-calls-btn-hangup:hover { background: var(--pmfa-calls-danger-strong); }\n.pmfa-calls-dialpad { width: 100%; padding: 20px; display: flex; flex-direction: column; gap: 16px; }\n.pmfa-calls-input { width: 100%; border: none; background: transparent; color: var(--pmfa-calls-fg); font-variant-numeric: tabular-nums; font-size: 22px; text-align: center; letter-spacing: 0.03em; padding: 6px 2px 10px; border-bottom: 1px solid var(--pmfa-calls-border); }\n.pmfa-calls-input:focus { outline: none; border-bottom-color: var(--pmfa-calls-accent); }\n.pmfa-calls-input::placeholder { color: var(--pmfa-calls-muted); opacity: 0.6; }\n.pmfa-calls-keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; justify-items: center; }\n.pmfa-calls-key { appearance: none; border: none; margin: 0; width: 62px; height: 48px; border-radius: var(--pmfa-calls-radius); background: transparent; color: var(--pmfa-calls-fg); font-family: var(--pmfa-calls-font); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.12s ease; }\n.pmfa-calls-key:hover { background: var(--pmfa-calls-accent-soft); }\n.pmfa-calls-key-digit { font-size: 21px; font-weight: 500; font-variant-numeric: tabular-nums; line-height: 1.1; }\n.pmfa-calls-dial-actions { display: flex; align-items: center; justify-content: center; gap: 18px; }\n.pmfa-calls-dialpad .pmfa-calls-btn-answer { animation: none; }\n.pmfa-calls-key-erase { color: var(--pmfa-calls-muted); }\n.pmfa-calls-key-erase:hover { color: var(--pmfa-calls-fg); background: var(--pmfa-calls-accent-soft); }\n.pmfa-calls-key-erase svg { width: 20px; height: 20px; }\n.pmfa-calls-popout { display: flex; flex-direction: column; align-items: center; gap: 12px; height: 100%; padding: 12px; background: var(--pmfa-calls-stage); }\n.pmfa-calls-popout .pmfa-calls-stage { flex: 1; min-height: 0; aspect-ratio: auto; }\n.pmfa-calls-popbar { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; padding: 8px 8px 8px 14px; border-radius: var(--pmfa-calls-radius); border: 1px solid var(--pmfa-calls-stage-line); background: var(--pmfa-calls-stage); color: var(--pmfa-calls-stage-fg); font-size: 13px; }\n@keyframes pmfa-calls-rise { from { opacity: 0; transform: translateY(8px) scale(0.97); } to { opacity: 1; transform: translateY(0) scale(1); } }\n@media (prefers-reduced-motion: reduce) {\n .pmfa-calls *, .pmfa-calls *::before, .pmfa-calls *::after { animation: none !important; transition: none !important; }\n}\n}\n";
|
|
200
|
+
export {};
|
|
201
|
+
//# sourceMappingURL=calls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calls.d.ts","sourceRoot":"","sources":["../src/calls.tsx"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAOL,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAKf,KAAK,eAAe,GAAG;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,eAAe,CAAC;CACnD,CAAC;AA4JF;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,GAAE,QAAQ,GAAG,SAA+B,GACjD,IAAI,CAON;AAyGD,wEAAwE;AACxE,MAAM,MAAM,cAAc,GAAG,CAC3B,IAAI,EAAE,MAAM,KACT,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAiMtD,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,aAAa,EACb,SAAS,EACT,SAAiB,EACjB,WAAW,GACZ,EAAE,qBAAqB,sCA8SvB;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,aAAa,CAAC,GACtD,MAAM,CAkBR;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAO3D;AAuDD,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,YAAY,CAAC,CAAC;IAC9D;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE;QAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;QAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;QAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS;YACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;YACrB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;YAC/B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;SAC9B,EAAE,CAAC;KACL,KAAK,SAAS,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EACxB,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,aAAa,EACb,SAAS,EACT,UAAU,EACV,WAAW,GACZ,EAAE,cAAc,sCAuJhB;AAID,MAAM,WAAW,yBAA0B,SAAQ,eAAe;IAChE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,GAAG,SAAS,CAAC;CACtE;AAqCD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,UAAU,EACV,gBAAgB,EAChB,SAAS,EACT,UAAU,GACX,EAAE,yBAAyB,sCA2B3B;AAED,MAAM,WAAW,oBAAqB,SAAQ,eAAe;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,WAAW,EAAE,eAAe,KAAK,MAAM,GAAG,SAAS,CAAC;CAC7E;AAYD,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,EAC9B,UAAU,EACV,gBAAgB,EAChB,SAAS,EACT,WAAW,GACZ,EAAE,oBAAoB,sCAoDtB;AAID,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,SAAS,GACV,EAAE,iBAAiB,sCAiKnB;AAmBD,MAAM,WAAW,YAAa,SAAQ,eAAe;IACnD,gCAAgC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,EACtB,UAAU,EACV,gBAAgB,EAChB,YAAiB,EACjB,UAAiB,EACjB,QAAQ,EACR,SAAS,GACV,EAAE,YAAY,+BAkGd;AAID,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAC;CACxC;AAED;;;;;GAKG;AACH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,SAAS,GAAG,gBAAgB,CA4C9D;AAID,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC5D,0DAA0D;IAC1D,QAAQ,CAAC,aAAa,CAAC,EAAE,cAAc,CAAC;IACxC,kEAAkE;IAClE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;IACrD;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,qCAAqC;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,YAAY,CAAC,CAAC;CAC/D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC1B,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,MAAM,EAAE,WAAkB,EAC1B,WAAW,EACX,SAAiB,EACjB,gBAAuB,EACvB,SAAS,EACT,UAAU,GACX,EAAE,gBAAgB,sCA0GlB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,wygBA0KxB,CAAC"}
|