@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { PolymorfaClient, type PolymorfaClientSnapshot, type PolymorfaPermission } from "@polymorfa/browser";
|
|
3
|
+
import { type Appearance, type AppearanceInput, type Locale } from "@polymorfa/ui";
|
|
4
|
+
export interface PolymorfaReactConfiguration {
|
|
5
|
+
readonly appearance: Appearance;
|
|
6
|
+
readonly locale: Locale;
|
|
7
|
+
}
|
|
8
|
+
export interface PolymorfaProviderProps {
|
|
9
|
+
readonly appearance?: AppearanceInput;
|
|
10
|
+
readonly locale?: Locale;
|
|
11
|
+
/**
|
|
12
|
+
* Your handler's token route, for example `/api/polymorfa/token`. The
|
|
13
|
+
* provider fetches a client token from it, refreshes it before expiry and
|
|
14
|
+
* shares it with every component inside.
|
|
15
|
+
*/
|
|
16
|
+
readonly tokenEndpoint?: string;
|
|
17
|
+
/** A client you created yourself; takes precedence over `tokenEndpoint`. */
|
|
18
|
+
readonly client?: PolymorfaClient;
|
|
19
|
+
readonly children?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export declare function PolymorfaProvider({ appearance, locale, tokenEndpoint, client, children, }: PolymorfaProviderProps): import("react").JSX.Element;
|
|
22
|
+
export declare function usePolymorfa(): PolymorfaReactConfiguration;
|
|
23
|
+
/**
|
|
24
|
+
* The provider's client: token refresh, `can()`, and the browser Messaging
|
|
25
|
+
* client. Throws outside `<PolymorfaProvider tokenEndpoint>`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function usePolymorfaClient(): PolymorfaClient;
|
|
28
|
+
/** The provider's client, or `undefined` without one. */
|
|
29
|
+
export declare function useOptionalPolymorfaClient(): PolymorfaClient | undefined;
|
|
30
|
+
export interface PolymorfaPermissions {
|
|
31
|
+
/** `unknown` without a provider client: controls render and the server decides. */
|
|
32
|
+
readonly status: PolymorfaClientSnapshot["status"] | "unknown";
|
|
33
|
+
readonly snapshot: PolymorfaClientSnapshot | undefined;
|
|
34
|
+
/** Whether the grant allows `permission`. True without a provider client. */
|
|
35
|
+
can(permission: PolymorfaPermission): boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What the current token allows, for rendering only. The server enforces
|
|
39
|
+
* every permission again; this never widens access.
|
|
40
|
+
*/
|
|
41
|
+
export declare function usePermissions(): PolymorfaPermissions;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a control may render. Once a token has been minted without
|
|
44
|
+
* `permission`, logs one development warning naming the component.
|
|
45
|
+
*/
|
|
46
|
+
export declare function usePermission(component: string, control: string, permission: PolymorfaPermission): boolean;
|
|
47
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EACL,eAAe,EAEf,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAMD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,UAAU,EACV,MAAM,EACN,aAAa,EACb,MAAM,EACN,QAAQ,GACT,EAAE,sBAAsB,+BAsBxB;AAgBD,wBAAgB,YAAY,IAAI,2BAA2B,CAQ1D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAOpD;AAED,yDAAyD;AACzD,wBAAgB,0BAA0B,IAAI,eAAe,GAAG,SAAS,CAExE;AAKD,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC;IAC/D,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,GAAG,SAAS,CAAC;IACvD,6EAA6E;IAC7E,GAAG,CAAC,UAAU,EAAE,mBAAmB,GAAG,OAAO,CAAC;CAC/C;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,oBAAoB,CAkBrD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,mBAAmB,GAC9B,OAAO,CAST"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useSyncExternalStore, } from "react";
|
|
3
|
+
import { PolymorfaClient, warnMissingPermission, } from "@polymorfa/browser";
|
|
4
|
+
import { createLocale, defineAppearance, } from "@polymorfa/ui";
|
|
5
|
+
import { safeSubscribe, useOwnedController } from "./hooks.js";
|
|
6
|
+
const Context = createContext(undefined);
|
|
7
|
+
const ClientContext = createContext(undefined);
|
|
8
|
+
export function PolymorfaProvider({ appearance, locale, tokenEndpoint, client, children, }) {
|
|
9
|
+
const value = useMemo(() => ({
|
|
10
|
+
appearance: defineAppearance(appearance),
|
|
11
|
+
locale: locale ?? createLocale("en"),
|
|
12
|
+
}), [appearance, locale]);
|
|
13
|
+
const owned = useOwnedClient(client === undefined ? tokenEndpoint : undefined);
|
|
14
|
+
const resolved = client ?? owned;
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
resolved?.start();
|
|
17
|
+
}, [resolved]);
|
|
18
|
+
return (_jsx(Context.Provider, { value: value, children: _jsx(ClientContext.Provider, { value: resolved, children: children }) }));
|
|
19
|
+
}
|
|
20
|
+
/** One owned client per endpoint, safe under StrictMode. */
|
|
21
|
+
function useOwnedClient(tokenEndpoint) {
|
|
22
|
+
return useOwnedController(undefined, tokenEndpoint === undefined
|
|
23
|
+
? undefined
|
|
24
|
+
: () => new PolymorfaClient({ tokenEndpoint }));
|
|
25
|
+
}
|
|
26
|
+
// One shared default keeps memoized components stable without a provider.
|
|
27
|
+
let defaultConfiguration;
|
|
28
|
+
export function usePolymorfa() {
|
|
29
|
+
const value = useContext(Context);
|
|
30
|
+
if (value !== undefined)
|
|
31
|
+
return value;
|
|
32
|
+
defaultConfiguration ??= {
|
|
33
|
+
appearance: defineAppearance(),
|
|
34
|
+
locale: createLocale("en"),
|
|
35
|
+
};
|
|
36
|
+
return defaultConfiguration;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The provider's client: token refresh, `can()`, and the browser Messaging
|
|
40
|
+
* client. Throws outside `<PolymorfaProvider tokenEndpoint>`.
|
|
41
|
+
*/
|
|
42
|
+
export function usePolymorfaClient() {
|
|
43
|
+
const client = useContext(ClientContext);
|
|
44
|
+
if (client === undefined)
|
|
45
|
+
throw new Error('usePolymorfaClient() needs <PolymorfaProvider tokenEndpoint="/api/polymorfa/token">.');
|
|
46
|
+
return client;
|
|
47
|
+
}
|
|
48
|
+
/** The provider's client, or `undefined` without one. */
|
|
49
|
+
export function useOptionalPolymorfaClient() {
|
|
50
|
+
return useContext(ClientContext);
|
|
51
|
+
}
|
|
52
|
+
const NO_SUBSCRIPTION = () => () => undefined;
|
|
53
|
+
const NO_SNAPSHOT = () => undefined;
|
|
54
|
+
/**
|
|
55
|
+
* What the current token allows, for rendering only. The server enforces
|
|
56
|
+
* every permission again; this never widens access.
|
|
57
|
+
*/
|
|
58
|
+
export function usePermissions() {
|
|
59
|
+
const client = useContext(ClientContext);
|
|
60
|
+
const snapshot = useSyncExternalStore(client === undefined ? NO_SUBSCRIPTION : safeSubscribe(client), client?.getSnapshot ?? NO_SNAPSHOT, client?.getSnapshot ?? NO_SNAPSHOT);
|
|
61
|
+
return useMemo(() => ({
|
|
62
|
+
status: snapshot?.status ?? "unknown",
|
|
63
|
+
snapshot,
|
|
64
|
+
can: (permission) => snapshot === undefined
|
|
65
|
+
? true
|
|
66
|
+
: snapshot.grant?.allow.includes(permission) === true,
|
|
67
|
+
}), [snapshot]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether a control may render. Once a token has been minted without
|
|
71
|
+
* `permission`, logs one development warning naming the component.
|
|
72
|
+
*/
|
|
73
|
+
export function usePermission(component, control, permission) {
|
|
74
|
+
const permissions = usePermissions();
|
|
75
|
+
const allowed = permissions.can(permission);
|
|
76
|
+
const minted = permissions.snapshot?.grant !== undefined;
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (minted && !allowed)
|
|
79
|
+
warnMissingPermission(component, control, permission);
|
|
80
|
+
}, [minted, allowed, component, control, permission]);
|
|
81
|
+
return allowed;
|
|
82
|
+
}
|
package/dist/dropin.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { InboxController, type ConnectWhatsAppOptions, type ConnectWhatsAppResult, type InboxContact, type InboxConversation, type InboxDataSource, type TemplateDraft } from "@polymorfa/browser";
|
|
2
|
+
import { type SlotClassNames } from "@polymorfa/ui";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
import { type ComposeBoxProps, type Slots } from "./components.js";
|
|
5
|
+
/** Initials avatar, or the image when `src` is set. */
|
|
6
|
+
export declare function Avatar({ id, name, src, size, slots, }: {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly src?: string;
|
|
10
|
+
readonly size?: "s" | "l";
|
|
11
|
+
readonly slots: Slots;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
export interface ConversationListProps {
|
|
14
|
+
readonly controller: InboxController;
|
|
15
|
+
readonly onSelect?: (conversation: InboxConversation) => void;
|
|
16
|
+
/** Heading above the list. Defaults to the locale's `inbox.title`. */
|
|
17
|
+
readonly title?: ReactNode;
|
|
18
|
+
/** Show the search field. Defaults to `true`. */
|
|
19
|
+
readonly search?: boolean;
|
|
20
|
+
readonly className?: string;
|
|
21
|
+
readonly classNames?: SlotClassNames;
|
|
22
|
+
}
|
|
23
|
+
/** The inbox's conversation list, usable on its own. */
|
|
24
|
+
export declare function ConversationList({ controller, onSelect, title, search, className, classNames, }: ConversationListProps): import("react").JSX.Element;
|
|
25
|
+
export interface ContactPanelProps {
|
|
26
|
+
readonly conversation: InboxConversation;
|
|
27
|
+
/** Loads the contact. Defaults to the inbox source's `contact()`. */
|
|
28
|
+
readonly load?: (conversation: InboxConversation, signal: AbortSignal) => Promise<InboxContact | undefined>;
|
|
29
|
+
readonly onClose?: () => void;
|
|
30
|
+
/** Extra content under the details, such as your CRM fields. */
|
|
31
|
+
readonly children?: ReactNode;
|
|
32
|
+
/** Actions under the name, for example a `<CallButton/>`. */
|
|
33
|
+
readonly actions?: ReactNode;
|
|
34
|
+
readonly className?: string;
|
|
35
|
+
readonly classNames?: SlotClassNames;
|
|
36
|
+
}
|
|
37
|
+
/** Contact details for a conversation. */
|
|
38
|
+
export declare function ContactPanel({ conversation, load, onClose, children, actions, className, classNames, }: ContactPanelProps): import("react").JSX.Element;
|
|
39
|
+
export interface InboxProps {
|
|
40
|
+
/**
|
|
41
|
+
* Where conversations come from. Defaults to your handler's history and
|
|
42
|
+
* events routes, through the provider's client.
|
|
43
|
+
*/
|
|
44
|
+
readonly source?: InboxDataSource;
|
|
45
|
+
/** Supply your own controller to drive the inbox from outside. */
|
|
46
|
+
readonly controller?: InboxController;
|
|
47
|
+
/** Open this conversation first. */
|
|
48
|
+
readonly defaultConversationId?: string;
|
|
49
|
+
readonly onSelect?: (conversation: InboxConversation | undefined) => void;
|
|
50
|
+
/** Show the contact panel toggle. Defaults to `true`. */
|
|
51
|
+
readonly contactPanel?: boolean;
|
|
52
|
+
/** Extra content inside the contact panel. */
|
|
53
|
+
readonly renderContact?: (conversation: InboxConversation) => ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* Actions in the thread header. Defaults to a `<CallButton/>` when the
|
|
56
|
+
* grant allows `voip_place` and the conversation has a phone number.
|
|
57
|
+
*/
|
|
58
|
+
readonly renderActions?: (conversation: InboxConversation) => ReactNode;
|
|
59
|
+
readonly title?: ReactNode;
|
|
60
|
+
readonly composerProps?: Omit<ComposeBoxProps, "controller" | "createController" | "conversation">;
|
|
61
|
+
readonly className?: string;
|
|
62
|
+
readonly classNames?: SlotClassNames;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A complete inbox: conversation list, chat and contact panel. Controls the
|
|
66
|
+
* token does not allow are hidden, with one development warning each.
|
|
67
|
+
*/
|
|
68
|
+
export declare function Inbox({ source, controller, defaultConversationId, onSelect, contactPanel, renderContact, renderActions, title, composerProps, className, classNames, }: InboxProps): import("react").JSX.Element;
|
|
69
|
+
export interface ConnectWhatsAppButtonProps extends Pick<ConnectWhatsAppOptions, "target" | "open"> {
|
|
70
|
+
readonly children?: ReactNode;
|
|
71
|
+
readonly onConnect?: (result: ConnectWhatsAppResult) => void;
|
|
72
|
+
readonly onError?: (error: unknown) => void;
|
|
73
|
+
readonly className?: string;
|
|
74
|
+
readonly classNames?: SlotClassNames;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Creates a QuickLink through your handler and opens Polymorfa's hosted
|
|
78
|
+
* pairing page. The pairing UI is never embedded.
|
|
79
|
+
*/
|
|
80
|
+
export declare function ConnectWhatsAppButton({ target, open, children, onConnect, onError, className, classNames, }: ConnectWhatsAppButtonProps): import("react").JSX.Element | null;
|
|
81
|
+
export interface SessionStatusProps {
|
|
82
|
+
/** Show a Retry button after a failure. Defaults to `true`. */
|
|
83
|
+
readonly retry?: boolean;
|
|
84
|
+
readonly className?: string;
|
|
85
|
+
readonly classNames?: SlotClassNames;
|
|
86
|
+
}
|
|
87
|
+
/** The provider's connection state as a badge, with a retry prompt. */
|
|
88
|
+
export declare function SessionStatus({ retry, className, classNames, }: SessionStatusProps): import("react").JSX.Element;
|
|
89
|
+
export interface TemplateManagerProps {
|
|
90
|
+
/** Starting draft for "New template". */
|
|
91
|
+
readonly newDraft?: TemplateDraft;
|
|
92
|
+
readonly className?: string;
|
|
93
|
+
readonly classNames?: SlotClassNames;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Lists your project's templates and edits one with `<TemplateBuilder/>`,
|
|
97
|
+
* through your handler's `templates` route.
|
|
98
|
+
*/
|
|
99
|
+
export declare function TemplateManager({ newDraft, className, classNames, }: TemplateManagerProps): import("react").JSX.Element | null;
|
|
100
|
+
//# sourceMappingURL=dropin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dropin.d.ts","sourceRoot":"","sources":["../src/dropin.tsx"],"names":[],"mappings":"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"}
|
package/dist/dropin.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { InboxController, MessageComposerController, TemplateBuilderController, connectWhatsApp, createConversationComposerActions, createHandlerInboxSource, createSameOriginTemplateBuilderTransport, } from "@polymorfa/browser";
|
|
3
|
+
import { formatDayLabel, formatMessageTime, } from "@polymorfa/ui";
|
|
4
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
|
|
5
|
+
import { ComposeBox, Icon, MessageList, TemplateBuilder, text, useShell, useSlots, } from "./components.js";
|
|
6
|
+
import { useOptionalPolymorfaClient, usePermission, usePermissions, usePolymorfa, usePolymorfaClient, } from "./context.js";
|
|
7
|
+
import { useController, useOwnedController } from "./hooks.js";
|
|
8
|
+
import { CallButton } from "./call-button.js";
|
|
9
|
+
// ── Shared pieces ─────────────────────────────────────────────────────
|
|
10
|
+
function initials(name) {
|
|
11
|
+
const letters = name
|
|
12
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
13
|
+
.trim()
|
|
14
|
+
.split(/\s+/)
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.map((word) => Array.from(word)[0] ?? "");
|
|
17
|
+
const first = letters[0] ?? "";
|
|
18
|
+
const last = letters.length > 1 ? (letters[letters.length - 1] ?? "") : "";
|
|
19
|
+
return (first + last).toUpperCase() || "#";
|
|
20
|
+
}
|
|
21
|
+
function hue(id) {
|
|
22
|
+
let hash = 0;
|
|
23
|
+
for (const character of id)
|
|
24
|
+
hash = Math.imul(hash ^ character.charCodeAt(0), 2654435761);
|
|
25
|
+
return Math.round(Math.abs(hash % 360) * 137.508) % 360;
|
|
26
|
+
}
|
|
27
|
+
function displayName(conversation) {
|
|
28
|
+
return conversation.name ?? conversation.phoneNumber ?? conversation.id;
|
|
29
|
+
}
|
|
30
|
+
/** Initials avatar, or the image when `src` is set. */
|
|
31
|
+
export function Avatar({ id, name, src, size, slots, }) {
|
|
32
|
+
const slot = slots("avatar", size === undefined ? "pmfa-avatar" : `pmfa-avatar pmfa-avatar-${size}`);
|
|
33
|
+
const [failed, setFailed] = useState(false);
|
|
34
|
+
return (_jsx("span", { ...slot, style: {
|
|
35
|
+
...slot.style,
|
|
36
|
+
"--pmfa-avatar-hue": String(hue(id)),
|
|
37
|
+
}, "aria-hidden": "true", children: src !== undefined && !failed ? (_jsx("img", { src: src, alt: "", onError: () => setFailed(true) })) : (initials(name)) }));
|
|
38
|
+
}
|
|
39
|
+
function conversationTime(configuration, time, now) {
|
|
40
|
+
const today = new Date(now).toDateString() === new Date(time).toDateString();
|
|
41
|
+
if (today)
|
|
42
|
+
return formatMessageTime(time, configuration.locale.code) ?? "";
|
|
43
|
+
return formatDayLabel(time, configuration.locale.code, {
|
|
44
|
+
today: text(configuration, "chat.today"),
|
|
45
|
+
yesterday: text(configuration, "chat.yesterday"),
|
|
46
|
+
}, now);
|
|
47
|
+
}
|
|
48
|
+
/** The inbox's conversation list, usable on its own. */
|
|
49
|
+
export function ConversationList({ controller, onSelect, title, search = true, className, classNames, }) {
|
|
50
|
+
const snapshot = useController(controller);
|
|
51
|
+
const configuration = usePolymorfa();
|
|
52
|
+
const slots = useSlots(classNames);
|
|
53
|
+
const list = slots("conversationList", "pmfa-inbox-list");
|
|
54
|
+
const [query, setQuery] = useState("");
|
|
55
|
+
const headingId = useId();
|
|
56
|
+
const normalized = query.trim().toLowerCase();
|
|
57
|
+
const visible = useMemo(() => normalized === ""
|
|
58
|
+
? snapshot.conversations
|
|
59
|
+
: snapshot.conversations.filter((conversation) => [
|
|
60
|
+
conversation.name,
|
|
61
|
+
conversation.phoneNumber,
|
|
62
|
+
conversation.lastMessage?.text,
|
|
63
|
+
].some((value) => value?.toLowerCase().includes(normalized))), [snapshot.conversations, normalized]);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
return (_jsxs("section", { ...list, className: [list.className, className].filter(Boolean).join(" "), "aria-labelledby": headingId, "data-pmfa": "conversation-list", children: [_jsxs("div", { className: "pmfa-inbox-head", children: [_jsx("h2", { className: "pmfa-inbox-title", id: headingId, children: title ?? text(configuration, "inbox.title") }), search && (_jsxs("div", { ...slots("conversationSearch", "pmfa-search"), children: [_jsx(Icon, { name: "search" }), _jsx("input", { ...slots("input", "pmfa-input"), type: "search", value: query, placeholder: text(configuration, "inbox.search"), "aria-label": text(configuration, "inbox.search"), onChange: (event) => setQuery(event.currentTarget.value) })] }))] }), snapshot.status === "loading" || snapshot.status === "idle" ? (_jsxs("div", { className: "pmfa-skeleton", "aria-busy": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {}), _jsx("span", { className: "pmfa-sr", children: text(configuration, "status.loading") })] })) : snapshot.status === "error" ? (_jsxs("div", { className: "pmfa-inbox-state", role: "alert", children: [_jsx("p", { className: "pmfa-error", children: text(configuration, "inbox.loadError") }), _jsx("button", { type: "button", ...slots("button", "pmfa-btn"), onClick: () => void controller.load(), children: text(configuration, "common.retry") })] })) : visible.length === 0 ? (_jsxs("div", { ...slots("empty", "pmfa-inbox-state"), children: [_jsx(Icon, { name: normalized === "" ? "inbox" : "search" }), text(configuration, normalized === "" ? "inbox.empty" : "inbox.noMatches")] })) : (_jsxs("ul", { className: "pmfa-convs", children: [visible.map((conversation) => {
|
|
66
|
+
const name = displayName(conversation);
|
|
67
|
+
const selected = conversation.id === snapshot.selectedId;
|
|
68
|
+
const last = conversation.lastMessage;
|
|
69
|
+
const unread = conversation.unreadCount;
|
|
70
|
+
return (_jsx("li", { children: _jsxs("button", { type: "button", ...slots("conversationItem", "pmfa-conv"), "aria-current": selected ? "true" : undefined, ...(unread > 0 ? { "data-unread": "" } : {}), onClick: () => {
|
|
71
|
+
controller.select(conversation.id);
|
|
72
|
+
onSelect?.(conversation);
|
|
73
|
+
}, children: [_jsx(Avatar, { id: conversation.id, name: name, ...(conversation.avatarUrl === undefined
|
|
74
|
+
? {}
|
|
75
|
+
: { src: conversation.avatarUrl }), slots: slots }), _jsx("span", { className: "pmfa-conv-name", children: name }), _jsx("span", { className: "pmfa-conv-time", children: conversation.lastActivity > 0
|
|
76
|
+
? conversationTime(configuration, conversation.lastActivity, now)
|
|
77
|
+
: "" }), _jsx("span", { className: "pmfa-conv-preview", children: last === undefined
|
|
78
|
+
? (conversation.phoneNumber ?? "")
|
|
79
|
+
: `${last.direction === "outbound" ? text(configuration, "inbox.you") : ""}${last.text}` }), unread > 0 && (_jsxs("span", { ...slots("unreadBadge", "pmfa-badge"), children: [_jsx("span", { "aria-hidden": "true", children: unread > 99 ? "99+" : unread }), _jsx("span", { className: "pmfa-sr", children: text(configuration, "inbox.unread", {
|
|
80
|
+
count: String(unread),
|
|
81
|
+
}) })] }))] }) }, conversation.id));
|
|
82
|
+
}), snapshot.hasMore && normalized === "" && (_jsx("li", { className: "pmfa-loadmore", style: { alignItems: "center", padding: 8 }, children: _jsx("button", { type: "button", ...slots("loadMore", "pmfa-btn"), disabled: snapshot.status === "loading_more", onClick: () => void controller.loadMore(), children: text(configuration, "inbox.loadMore") }) }))] }))] }));
|
|
83
|
+
}
|
|
84
|
+
/** Contact details for a conversation. */
|
|
85
|
+
export function ContactPanel({ conversation, load, onClose, children, actions, className, classNames, }) {
|
|
86
|
+
const configuration = usePolymorfa();
|
|
87
|
+
const slots = useSlots(classNames);
|
|
88
|
+
const panel = slots("contactPanel", "pmfa-contact");
|
|
89
|
+
const [contact, setContact] = useState();
|
|
90
|
+
const headingId = useId();
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
setContact(undefined);
|
|
93
|
+
if (load === undefined)
|
|
94
|
+
return;
|
|
95
|
+
const abort = new AbortController();
|
|
96
|
+
load(conversation, abort.signal).then((value) => {
|
|
97
|
+
if (!abort.signal.aborted)
|
|
98
|
+
setContact(value);
|
|
99
|
+
}, () => undefined);
|
|
100
|
+
return () => abort.abort();
|
|
101
|
+
}, [conversation.id, load]);
|
|
102
|
+
const name = contact?.name ?? displayName(conversation);
|
|
103
|
+
const phone = contact?.phoneNumber ?? conversation.phoneNumber;
|
|
104
|
+
const rows = [];
|
|
105
|
+
if (phone !== undefined)
|
|
106
|
+
rows.push({ label: text(configuration, "inbox.phone"), value: phone });
|
|
107
|
+
if (contact?.email !== undefined)
|
|
108
|
+
rows.push({
|
|
109
|
+
label: text(configuration, "inbox.email"),
|
|
110
|
+
value: contact.email,
|
|
111
|
+
});
|
|
112
|
+
if (contact?.about !== undefined)
|
|
113
|
+
rows.push({
|
|
114
|
+
label: text(configuration, "inbox.about"),
|
|
115
|
+
value: contact.about,
|
|
116
|
+
});
|
|
117
|
+
for (const field of contact?.fields ?? [])
|
|
118
|
+
rows.push(field);
|
|
119
|
+
const avatar = contact?.avatarUrl ?? conversation.avatarUrl;
|
|
120
|
+
return (_jsxs("aside", { ...panel, className: [panel.className, className].filter(Boolean).join(" "), "aria-labelledby": headingId, "data-pmfa": "contact-panel", children: [_jsxs("div", { className: "pmfa-contact-head", children: [_jsx("h3", { id: headingId, children: text(configuration, "inbox.contact") }), onClose !== undefined && (_jsx("button", { type: "button", ...slots("button", "pmfa-btn pmfa-btn-ghost pmfa-btn-icon"), "aria-label": text(configuration, "inbox.hideContact"), onClick: onClose, children: _jsx(Icon, { name: "close" }) }))] }), _jsxs("div", { className: "pmfa-contact-card", children: [_jsx(Avatar, { id: conversation.id, name: name, ...(avatar === undefined ? {} : { src: avatar }), size: "l", slots: slots }), _jsx("p", { className: "pmfa-contact-name", children: name }), phone !== undefined && phone !== name && (_jsx("span", { className: "pmfa-contact-phone", children: phone })), actions !== undefined && (_jsx("div", { className: "pmfa-contact-actions", children: actions }))] }), rows.length > 0 && (_jsx("dl", { className: "pmfa-contact-fields", children: rows.map((row, index) => (_jsxs("div", { children: [_jsx("dt", { children: row.label }), _jsx("dd", { children: row.value })] }, `${row.label}-${index}`))) })), children] }));
|
|
121
|
+
}
|
|
122
|
+
// ── Thread ────────────────────────────────────────────────────────────
|
|
123
|
+
const noUpload = async () => {
|
|
124
|
+
throw new Error("Attachments need an upload adapter.");
|
|
125
|
+
};
|
|
126
|
+
function Thread({ inbox, conversation, slots, onBack, contactOpen, onToggleContact, showContactToggle, actions, composerProps, }) {
|
|
127
|
+
const configuration = usePolymorfa();
|
|
128
|
+
const controller = inbox.conversation(conversation);
|
|
129
|
+
const canSend = usePermission("Inbox", "composer", "send_message");
|
|
130
|
+
const ownedComposer = useOwnedController(undefined, canSend
|
|
131
|
+
? () => new MessageComposerController(createConversationComposerActions(controller, noUpload))
|
|
132
|
+
: undefined);
|
|
133
|
+
// A revoked permission hides the composer even if one was created.
|
|
134
|
+
const composer = canSend ? ownedComposer : undefined;
|
|
135
|
+
const threadRef = useRef(null);
|
|
136
|
+
const reply = useCallback((message) => {
|
|
137
|
+
composer?.setReplyTo(message.id);
|
|
138
|
+
threadRef.current?.querySelector("textarea")?.focus();
|
|
139
|
+
}, [composer]);
|
|
140
|
+
const name = displayName(conversation);
|
|
141
|
+
const sub = conversation.phoneNumber !== undefined && conversation.phoneNumber !== name
|
|
142
|
+
? conversation.phoneNumber
|
|
143
|
+
: undefined;
|
|
144
|
+
return (_jsxs("section", { ...slots("thread", "pmfa-thread"), ref: threadRef, "aria-label": name, "data-pmfa": "thread", children: [_jsxs("header", { ...slots("threadHeader", "pmfa-thread-head"), children: [_jsx("button", { type: "button", ...slots("backButton", "pmfa-btn pmfa-btn-ghost pmfa-btn-icon pmfa-back"), "aria-label": text(configuration, "inbox.back"), onClick: onBack, children: _jsx(Icon, { name: "back" }) }), _jsx(Avatar, { id: conversation.id, name: name, ...(conversation.avatarUrl === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { src: conversation.avatarUrl }), size: "s", slots: slots }), _jsxs("div", { className: "pmfa-thread-who", children: [_jsx("span", { className: "pmfa-thread-name", children: name }), sub !== undefined && _jsx("span", { className: "pmfa-thread-sub", children: sub })] }), _jsxs("div", { className: "pmfa-thread-actions", children: [actions, showContactToggle && onToggleContact !== undefined && (_jsx("button", { type: "button", ...slots("button", "pmfa-btn pmfa-btn-ghost pmfa-btn-icon"), "aria-label": text(configuration, contactOpen ? "inbox.hideContact" : "inbox.showContact"), title: text(configuration, "inbox.contact"), "aria-expanded": contactOpen, onClick: onToggleContact, children: _jsx(Icon, { name: "info" }) }))] })] }), _jsx(MessageList, { controller: controller, ...(composer === undefined ? {} : { onReply: reply }) }), _jsx("div", { className: "pmfa-thread-foot", children: composer !== undefined ? (_jsx(ComposeBox, { attachments: false, voiceNotes: false, ...composerProps, controller: composer, conversation: controller })) : (_jsx("p", { className: "pmfa-readonly", children: text(configuration, "inbox.readOnly") })) })] }));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A complete inbox: conversation list, chat and contact panel. Controls the
|
|
150
|
+
* token does not allow are hidden, with one development warning each.
|
|
151
|
+
*/
|
|
152
|
+
export function Inbox({ source, controller, defaultConversationId, onSelect, contactPanel = true, renderContact, renderActions, title, composerProps, className, classNames, }) {
|
|
153
|
+
const client = useOptionalPolymorfaClient();
|
|
154
|
+
const permissions = usePermissions();
|
|
155
|
+
if (controller === undefined && source === undefined && client === undefined)
|
|
156
|
+
throw new Error('<Inbox/> needs <PolymorfaProvider tokenEndpoint="/api/polymorfa/token"> or a source.');
|
|
157
|
+
const inbox = useOwnedController(controller, () => {
|
|
158
|
+
const created = new InboxController(source ?? createHandlerInboxSource(client));
|
|
159
|
+
if (defaultConversationId !== undefined)
|
|
160
|
+
created.select(defaultConversationId);
|
|
161
|
+
return created;
|
|
162
|
+
});
|
|
163
|
+
const canRead = usePermission("Inbox", "conversations", "read_messages");
|
|
164
|
+
const ready = permissions.status === "unknown" ||
|
|
165
|
+
permissions.snapshot?.grant !== undefined;
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!ready || !canRead)
|
|
168
|
+
return;
|
|
169
|
+
const status = inbox.getSnapshot().status;
|
|
170
|
+
if (status === "idle")
|
|
171
|
+
void inbox.load();
|
|
172
|
+
}, [inbox, ready, canRead]);
|
|
173
|
+
const snapshot = useController(inbox);
|
|
174
|
+
const configuration = usePolymorfa();
|
|
175
|
+
const slots = useSlots(classNames);
|
|
176
|
+
const root = useShell(slots, "inbox", "pmfa-inbox", className);
|
|
177
|
+
const contactAllowed = usePermission("Inbox", "contact panel", "read_contact");
|
|
178
|
+
const canContact = contactPanel && contactAllowed;
|
|
179
|
+
const canCall = permissions.can("voip_place");
|
|
180
|
+
const [contactOpen, setContactOpen] = useState(false);
|
|
181
|
+
const selected = snapshot.conversations.find((conversation) => conversation.id === snapshot.selectedId);
|
|
182
|
+
const latestSelect = useRef(onSelect);
|
|
183
|
+
latestSelect.current = onSelect;
|
|
184
|
+
const loadContact = useMemo(() => inbox.source.contact === undefined
|
|
185
|
+
? undefined
|
|
186
|
+
: (conversation, signal) => inbox.source.contact(conversation, signal), [inbox]);
|
|
187
|
+
const back = useCallback(() => {
|
|
188
|
+
inbox.select(undefined);
|
|
189
|
+
setContactOpen(false);
|
|
190
|
+
latestSelect.current?.(undefined);
|
|
191
|
+
}, [inbox]);
|
|
192
|
+
if (!canRead && permissions.snapshot?.grant !== undefined)
|
|
193
|
+
return (_jsx("div", { ...root, "data-pmfa": "inbox", "data-view": "list", children: _jsxs("div", { className: "pmfa-inbox-state", style: { gridColumn: "1 / -1" }, children: [_jsx(Icon, { name: "inbox" }), text(configuration, "inbox.empty")] }) }));
|
|
194
|
+
const actions = selected === undefined ? null : renderActions !== undefined ? (renderActions(selected)) : canCall && selected.phoneNumber !== undefined ? (_jsx(CallButton, { to: selected.phoneNumber, variant: "icon" })) : null;
|
|
195
|
+
const showPanel = canContact && contactOpen && selected !== undefined;
|
|
196
|
+
return (_jsxs("div", { ...root, "data-pmfa": "inbox", "data-view": selected === undefined ? "list" : "thread", ...(showPanel ? { "data-contact-open": "" } : {}), children: [_jsx(ConversationList, { controller: inbox, ...(title === undefined ? {} : { title }), ...(classNames === undefined ? {} : { classNames }), onSelect: (conversation) => latestSelect.current?.(conversation) }), selected === undefined ? (_jsx("section", { ...slots("thread", "pmfa-thread"), "data-pmfa": "thread", children: _jsxs("div", { className: "pmfa-inbox-state", children: [_jsx(Icon, { name: "inbox" }), text(configuration, "inbox.select")] }) })) : (_jsx(Thread, { inbox: inbox, conversation: selected, slots: slots, onBack: back, contactOpen: showPanel, onToggleContact: canContact ? () => setContactOpen((open) => !open) : undefined, showContactToggle: canContact, actions: actions, composerProps: composerProps }, selected.id)), showPanel && (_jsx(ContactPanel, { conversation: selected, ...(loadContact === undefined ? {} : { load: loadContact }), onClose: () => setContactOpen(false), ...(classNames === undefined ? {} : { classNames }), children: renderContact?.(selected) }))] }));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Creates a QuickLink through your handler and opens Polymorfa's hosted
|
|
200
|
+
* pairing page. The pairing UI is never embedded.
|
|
201
|
+
*/
|
|
202
|
+
export function ConnectWhatsAppButton({ target = "redirect", open, children, onConnect, onError, className, classNames, }) {
|
|
203
|
+
const client = usePolymorfaClient();
|
|
204
|
+
const configuration = usePolymorfa();
|
|
205
|
+
const slots = useSlots(classNames);
|
|
206
|
+
const root = useShell(slots, "connectButton", "pmfa-btn pmfa-btn-primary pmfa-connect", className);
|
|
207
|
+
const allowed = usePermission("ConnectWhatsAppButton", "button", "connect_whatsapp");
|
|
208
|
+
const [busy, setBusy] = useState(false);
|
|
209
|
+
const [failed, setFailed] = useState(false);
|
|
210
|
+
const errorId = useId();
|
|
211
|
+
if (!allowed)
|
|
212
|
+
return null;
|
|
213
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", ...root, "data-pmfa": "connect-whatsapp", "aria-busy": busy ? "true" : undefined, "aria-describedby": failed ? errorId : undefined, disabled: busy, onClick: () => {
|
|
214
|
+
setBusy(true);
|
|
215
|
+
setFailed(false);
|
|
216
|
+
connectWhatsApp(client, {
|
|
217
|
+
target,
|
|
218
|
+
...(open === undefined ? {} : { open }),
|
|
219
|
+
}).then((result) => {
|
|
220
|
+
setBusy(false);
|
|
221
|
+
onConnect?.(result);
|
|
222
|
+
}, (error) => {
|
|
223
|
+
setBusy(false);
|
|
224
|
+
setFailed(true);
|
|
225
|
+
onError?.(error);
|
|
226
|
+
});
|
|
227
|
+
}, children: [_jsx(Icon, { name: "link" }), busy
|
|
228
|
+
? text(configuration, "connect.opening")
|
|
229
|
+
: (children ?? text(configuration, "connect.button"))] }), failed && (_jsx("span", { id: errorId, role: "alert", ...slots("error", "pmfa pmfa-error"), style: { display: "block", marginTop: 6 }, children: text(configuration, "connect.failed") }))] }));
|
|
230
|
+
}
|
|
231
|
+
/** The provider's connection state as a badge, with a retry prompt. */
|
|
232
|
+
export function SessionStatus({ retry = true, className, classNames, }) {
|
|
233
|
+
const client = usePolymorfaClient();
|
|
234
|
+
const { snapshot } = usePermissions();
|
|
235
|
+
const configuration = usePolymorfa();
|
|
236
|
+
const slots = useSlots(classNames);
|
|
237
|
+
const root = useShell(slots, "sessionStatus", "pmfa-session", className);
|
|
238
|
+
const status = snapshot?.status ?? "idle";
|
|
239
|
+
const label = text(configuration, `status.${status}`);
|
|
240
|
+
const failed = status === "error" || status === "unauthenticated" || status === "retrying";
|
|
241
|
+
return (_jsxs("span", { ...root, role: "status", "data-pmfa": "session-status", "data-status": status, "aria-label": text(configuration, "status.label", { status: label }), children: [_jsx("span", { className: "pmfa-session-dot", "aria-hidden": "true" }), _jsx("span", { "aria-hidden": "true", children: label }), retry && failed && (_jsx("button", { type: "button", ...slots("button", "pmfa-btn pmfa-btn-ghost"), onClick: () => void client.refresh().catch(() => undefined), children: text(configuration, "common.retry") }))] }));
|
|
242
|
+
}
|
|
243
|
+
const DEFAULT_DRAFT = {
|
|
244
|
+
name: "new_template",
|
|
245
|
+
definition: {
|
|
246
|
+
version: 1,
|
|
247
|
+
kind: "standard",
|
|
248
|
+
category: "UTILITY",
|
|
249
|
+
language: "en_US",
|
|
250
|
+
body: "Hello {{name}}",
|
|
251
|
+
variables: [{ name: "name", type: "text", example: "Ada" }],
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Lists your project's templates and edits one with `<TemplateBuilder/>`,
|
|
256
|
+
* through your handler's `templates` route.
|
|
257
|
+
*/
|
|
258
|
+
export function TemplateManager({ newDraft = DEFAULT_DRAFT, className, classNames, }) {
|
|
259
|
+
const client = usePolymorfaClient();
|
|
260
|
+
const configuration = usePolymorfa();
|
|
261
|
+
const slots = useSlots(classNames);
|
|
262
|
+
const root = useShell(slots, "templateManager", "pmfa-tm", className);
|
|
263
|
+
const allowed = usePermission("TemplateManager", "templates", "manage_templates");
|
|
264
|
+
const transport = useMemo(() => createSameOriginTemplateBuilderTransport({
|
|
265
|
+
path: client.url("templates"),
|
|
266
|
+
fetch: client.fetch,
|
|
267
|
+
}), [client]);
|
|
268
|
+
const [templates, setTemplates] = useState();
|
|
269
|
+
const [failed, setFailed] = useState(false);
|
|
270
|
+
const [editing, setEditing] = useState();
|
|
271
|
+
const reload = useCallback(() => {
|
|
272
|
+
const abort = new AbortController();
|
|
273
|
+
setFailed(false);
|
|
274
|
+
transport.list(abort.signal).then(setTemplates, () => setFailed(true));
|
|
275
|
+
return () => abort.abort();
|
|
276
|
+
}, [transport]);
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (allowed && editing === undefined)
|
|
279
|
+
return reload();
|
|
280
|
+
return undefined;
|
|
281
|
+
}, [allowed, editing, reload]);
|
|
282
|
+
if (!allowed)
|
|
283
|
+
return null;
|
|
284
|
+
if (editing !== undefined)
|
|
285
|
+
return (_jsxs("div", { ...root, "data-pmfa": "template-manager", children: [_jsx("div", { className: "pmfa-tm-head", children: _jsxs("button", { type: "button", ...slots("backButton", "pmfa-btn pmfa-btn-ghost"), onClick: () => setEditing(undefined), children: [_jsx(Icon, { name: "back" }), text(configuration, "templates.back")] }) }), _jsx(TemplateEditor, { transport: transport, ...(editing.id === undefined ? {} : { templateId: editing.id }), draft: newDraft, ...(classNames === undefined ? {} : { classNames }) }, editing.key)] }));
|
|
286
|
+
return (_jsxs("div", { ...root, "data-pmfa": "template-manager", children: [_jsxs("div", { className: "pmfa-tm-head", children: [_jsx("h2", { children: text(configuration, "templates.list") }), _jsxs("button", { type: "button", ...slots("primaryButton", "pmfa-btn pmfa-btn-primary"), onClick: () => setEditing({ key: Date.now() }), children: [_jsx(Icon, { name: "plus" }), text(configuration, "templates.new")] })] }), failed ? (_jsxs("div", { className: "pmfa-inbox-state", role: "alert", children: [_jsx("p", { className: "pmfa-error", children: text(configuration, "templates.loadError") }), _jsx("button", { type: "button", ...slots("button", "pmfa-btn"), onClick: reload, children: text(configuration, "common.retry") })] })) : templates === undefined ? (_jsxs("div", { className: "pmfa-skeleton", "aria-busy": "true", children: [_jsx("span", {}), _jsx("span", {})] })) : templates.length === 0 ? (_jsx("div", { ...slots("empty", "pmfa-inbox-state"), children: text(configuration, "templates.empty") })) : (_jsx("ul", { ...slots("templateList", "pmfa-tm-list"), children: templates.map((template) => (_jsx("li", { children: _jsxs("button", { type: "button", ...slots("templateItem", "pmfa-tm-item"), onClick: () => setEditing({ id: template.id, key: Date.now() }), children: [_jsx("span", { className: "pmfa-tm-name", children: template.name }), _jsx("span", { className: "pmfa-tm-body", children: template.definition.body }), _jsxs("span", { className: "pmfa-tm-meta", children: [_jsx("span", { className: "pmfa-tag", "data-status": template.status, children: template.status }), _jsx("span", { className: "pmfa-tag", children: template.category }), _jsx("span", { className: "pmfa-tag", children: template.language })] })] }) }, template.id))) }))] }));
|
|
287
|
+
}
|
|
288
|
+
function TemplateEditor({ transport, templateId, draft, classNames, }) {
|
|
289
|
+
const controller = useOwnedController(undefined, () => {
|
|
290
|
+
const created = new TemplateBuilderController(transport);
|
|
291
|
+
if (templateId === undefined)
|
|
292
|
+
created.create(draft);
|
|
293
|
+
else
|
|
294
|
+
void created.load(templateId);
|
|
295
|
+
return created;
|
|
296
|
+
});
|
|
297
|
+
return (_jsx(TemplateBuilder, { controller: controller, ...(classNames === undefined ? {} : { classNames }) }));
|
|
298
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ReactController<TSnapshot> {
|
|
2
|
+
getSnapshot(): TSnapshot;
|
|
3
|
+
subscribe(listener: () => void): () => void;
|
|
4
|
+
dispose(): void;
|
|
5
|
+
}
|
|
6
|
+
type Subscribable = {
|
|
7
|
+
subscribe(listener: () => void): () => void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* `controller.subscribe`, except that a disposed controller yields a no-op
|
|
11
|
+
* subscription. Under StrictMode an owned controller can be disposed and
|
|
12
|
+
* replaced while a child is still subscribing to it; the child re-renders
|
|
13
|
+
* with the replacement right after.
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export declare function safeSubscribe(controller: Subscribable): (listener: () => void) => () => void;
|
|
17
|
+
export declare function useController<TSnapshot>(controller: ReactController<TSnapshot>): TSnapshot;
|
|
18
|
+
/**
|
|
19
|
+
* The passed `controller`, or one made by `create` that this component owns
|
|
20
|
+
* and disposes. Safe under StrictMode and `<Activity>`: when an effect
|
|
21
|
+
* cleanup disposes the owned controller and the effect runs again, a fresh
|
|
22
|
+
* controller replaces it and the component re-renders with it.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export declare function useOwnedController<T extends {
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}>(controller: T | undefined, create: (() => T) | undefined): T | undefined;
|
|
28
|
+
export declare function useResolvedController<TSnapshot, TController extends ReactController<TSnapshot>>(controller: TController | undefined, createController: (() => TController) | undefined): TController;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe,CAAC,SAAS;IACxC,WAAW,IAAI,SAAS,CAAC;IACzB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,KAAK,YAAY,GAAG;IAAE,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAAE,CAAC;AAMpE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,YAAY,GACvB,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAetC;AAED,wBAAgB,aAAa,CAAC,SAAS,EACrC,UAAU,EAAE,eAAe,CAAC,SAAS,CAAC,GACrC,SAAS,CAMX;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,OAAO,IAAI,IAAI,CAAA;CAAE,EAC9D,UAAU,EAAE,CAAC,GAAG,SAAS,EACzB,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,GAC5B,CAAC,GAAG,SAAS,CAwBf;AAED,wBAAgB,qBAAqB,CACnC,SAAS,EACT,WAAW,SAAS,eAAe,CAAC,SAAS,CAAC,EAE9C,UAAU,EAAE,WAAW,GAAG,SAAS,EACnC,gBAAgB,EAAE,CAAC,MAAM,WAAW,CAAC,GAAG,SAAS,GAChD,WAAW,CAMb"}
|