@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 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
+ }
@@ -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":"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"}