@chatlychat/widget-react 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # @chatlychat/widget-react — changelog
2
+
3
+ ## 0.1.0 — unreleased
4
+
5
+ - Initial release: `<ChatlyWidget />`, `useChatly()`, `useChatlyEvent()`.
6
+ - React 18 StrictMode safe (one boot, not two), tears the widget down on
7
+ unmount, and turns an `identity`/`locale` prop change into `identify()` /
8
+ `setLocale()` rather than a re-init.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chatly Engineering
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,203 @@
1
- # Temporary Holding Version
1
+ # @chatlychat/widget-react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Chatly chat widget as a React component.
4
+
5
+ ```bash
6
+ pnpm add @chatlychat/widget-react
7
+ ```
8
+
9
+ ```tsx
10
+ import { ChatlyWidget, useChatly } from '@chatlychat/widget-react';
11
+
12
+ export function App() {
13
+ return (
14
+ <>
15
+ {/* One of these, high in the tree. */}
16
+ <ChatlyWidget publicId="acme-web" visitor={{ externalId: user.id, email: user.email }} />
17
+ <Routes />
18
+ </>
19
+ );
20
+ }
21
+
22
+ export function HelpButton() {
23
+ const chatly = useChatly();
24
+ return <button onClick={() => chatly.open('help')}>Help</button>;
25
+ }
26
+ ```
27
+
28
+ That is the whole integration. No `<script>` tag, no `window.Chatly`, no
29
+ effect of your own — the component injects the loader, mounts the widget and
30
+ tears it down when it unmounts.
31
+
32
+ ## Why this exists
33
+
34
+ Without it, the integration is an effect you write by hand:
35
+
36
+ ```tsx
37
+ // Don't. This is the version that was in our own docs, and it has three bugs.
38
+ useEffect(() => {
39
+ window.Chatly?.init({ publicId });
40
+ return () => window.Chatly?.destroy();
41
+ }, [publicId]);
42
+ ```
43
+
44
+ 1. **It boots twice in StrictMode.** React 18 runs every effect mount →
45
+ cleanup → mount in development, so that is `init` → `destroy` → `init`:
46
+ two visitor sessions minted, and a launcher that visibly flickers.
47
+ 2. **A `theme` in the deps re-initialises on every render.** `theme={{…}}` is
48
+ a new object each time, so the widget is destroyed and rebuilt underneath a
49
+ visitor who was mid-sentence.
50
+ 3. **It can never identify anybody.** The only place to put an identity is
51
+ `init`, which has already run by the time your user signs in.
52
+
53
+ This component fixes all three, and the mechanism is shared with the custom
54
+ element in [`@chatlychat/widget-element`](../widget-element) so the two cannot
55
+ drift on the part that is hard.
56
+
57
+ ## React is a peer dependency
58
+
59
+ `react` is in `peerDependencies`, never `dependencies`, and that is a
60
+ deliberate exception to this repo's "no new dependencies" rule.
61
+
62
+ A real dependency resolves a **second copy of React** in any consumer whose
63
+ version differs by so much as a patch, and two Reacts on one page break hooks
64
+ with `Invalid hook call` — thrown from a component the consumer did not write
65
+ and cannot debug. A peer dependency on the framework the consumer is already
66
+ using adds nothing to their install and cannot duplicate.
67
+
68
+ The range is `^18.0.0 || ^19.0.0`. The component uses `useRef` and
69
+ `useEffect` and nothing newer, so it works on both.
70
+
71
+ Everything else here has **one** dependency, `@chatlychat/widget-element`, which
72
+ itself has none.
73
+
74
+ ## `<ChatlyWidget />`
75
+
76
+ Every [init option](https://chatlychat.com/docs/channel-web) is a prop:
77
+ `publicId` (required), `apiUrl`, `realtimeUrl`, `visitor`, `theme`,
78
+ `behavior`, `quickLinks`, `copy`, `copyByLocale`, `cssOverrides`.
79
+
80
+ Plus four of its own:
81
+
82
+ | prop | type |
83
+ | --------------------- | ------------------------------------------------------------- |
84
+ | `loaderUrl` | Where `loader.js` is served from. **Self-hosting: set this.** |
85
+ | `locale` | Calls `setLocale()` once the widget is up. |
86
+ | `inline` | Render the chat inside this component's own box. |
87
+ | `className` / `style` | Applied to the inline container. |
88
+
89
+ And the events, as callbacks: `onReady`, `onOpen`, `onClose`, `onMessage`,
90
+ `onConversation`, `onIdentified`, `onReset`, `onAction`, `onError`. Pass
91
+ inline arrow functions freely — they are read through a ref, so a new one on
92
+ every render costs nothing and the handler that fires is always the latest.
93
+
94
+ ### What a prop change actually does
95
+
96
+ This is the part worth knowing, because the three outcomes are different:
97
+
98
+ | changed prop | what happens |
99
+ | ----------------------- | ----------------------------------------------------- |
100
+ | `visitor` | `identify()`. Nothing is re-mounted. |
101
+ | `visitor` → `undefined` | `reset()` — a sign-out, so the next session is fresh. |
102
+ | `locale` | `setLocale()`. Nothing is re-mounted. |
103
+ | anything else | `init()` again, which **destroys and re-mounts**. |
104
+
105
+ A re-mount loses nothing durable — the conversation lives on the server — but
106
+ it closes an open panel. The widget has no `setTheme`, so there is no honest
107
+ alternative; what this component guarantees is that it only happens when a
108
+ value really changed, not when a parent re-rendered.
109
+
110
+ `loaderUrl` is the exception: changing it after load warns and does nothing,
111
+ because the bundle is already fetched and pointing the running widget at
112
+ another deployment would need a page reload.
113
+
114
+ ### Inline mounting
115
+
116
+ ```tsx
117
+ <ChatlyWidget publicId="acme-web" inline className="h-[600px] w-full rounded-lg border" />
118
+ ```
119
+
120
+ The chat fills the box instead of floating in the corner — a support page, a
121
+ sidebar, a full-page `/chat` route. In this mode there is no launcher, no
122
+ corner offset, and `close()` warns instead of working (hide or unmount your
123
+ own container instead).
124
+
125
+ This is the React shape of the widget's `mountTo` option, and it closes
126
+ `mountTo`'s one sharp edge: the container must exist when `init` runs, and a
127
+ selector that matches nothing makes the widget refuse to mount at all. A
128
+ container this component renders itself cannot be missing.
129
+
130
+ Give it a height. It has a 420px minimum so a container with none is still
131
+ usable.
132
+
133
+ ## `useChatly()`
134
+
135
+ Returns the widget's post-boot methods: `open`, `close`, `toggle`,
136
+ `identify`, `setUser`, `setCustomAttributes`, `deleteCustomAttribute`,
137
+ `setLabel`, `removeLabel`, `setLocale`, `track`, `registerPushToken`, `reset`.
138
+
139
+ The object is referentially stable, so it is safe in a dependency array.
140
+
141
+ `init` and `destroy` are deliberately **not** on it. The component owns the
142
+ lifecycle; calling them behind its back leaves it believing a widget is
143
+ mounted that is not, after which the next unmount does nothing and the next
144
+ mount does nothing.
145
+
146
+ Every method returns `void`, including `identify`, which the widget's own API
147
+ returns a promise from. Your call may be queued before the bundle has arrived
148
+ and there is nothing to await in that state — use `onIdentified` (or
149
+ `useChatlyEvent('identified', …)`) to learn that it landed, and `onError` to
150
+ learn that it did not.
151
+
152
+ Calling any of them with no widget mounted warns in the console and does
153
+ nothing. It never throws: a misconfigured chat must not break your click
154
+ handler.
155
+
156
+ ## `useChatlyEvent(event, handler)`
157
+
158
+ ```tsx
159
+ function UnreadBadge() {
160
+ const [unread, setUnread] = useState(0);
161
+ useChatlyEvent('message', () => setUnread((n) => n + 1));
162
+ useChatlyEvent('open', () => setUnread(0));
163
+ return unread > 0 ? <span className="badge">{unread}</span> : null;
164
+ }
165
+ ```
166
+
167
+ Subscribes for the life of the component. The handler is read through a ref,
168
+ so an inline arrow does not re-subscribe on every render.
169
+
170
+ `'ready'` fires immediately if the session is already up — the widget replays
171
+ the last one — which is what makes this usable from a component that mounts
172
+ long after the page did.
173
+
174
+ ## One widget per page
175
+
176
+ `window.Chatly` is a singleton, so there is one widget per page whatever you
177
+ render. Two mounted `<ChatlyWidget />`s with different options is a
178
+ misconfiguration: the **first** one owns the configuration, the second's
179
+ options are ignored (its event handlers still fire), and you get a console
180
+ warning saying so. Render one, high in the tree, and reach for `useChatly()`
181
+ everywhere else.
182
+
183
+ Two mounted with the _same_ options is harmless and silent — which is what
184
+ makes a `<ChatlyWidget />` inside a layout that remounts on navigation safe.
185
+
186
+ ## Server-side rendering
187
+
188
+ Safe. The component renders `null` (or an empty `<div>` when `inline`), and
189
+ everything that touches `window` happens in an effect, which never runs on the
190
+ server. There is no `'use client'` needed in Next.js App Router beyond the one
191
+ your own component already has if it uses hooks.
192
+
193
+ ## Self-hosting
194
+
195
+ Set `loaderUrl` to your deployment's. `GET /v1/channels/:id` returns the right
196
+ value as `embed.loaderUrl` — read it from there rather than typing a host, and
197
+ note that a running widget talks to **three** origins (widget host, API host,
198
+ realtime host); the same endpoint reports all of them as `connectOrigins` for
199
+ your CSP.
200
+
201
+ ## License
202
+
203
+ MIT — see `LICENSE`.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `<ChatlyWidget />` — the widget as a React component.
3
+ *
4
+ * ── What this replaces ──────────────────────────────────────────────────
5
+ *
6
+ * Without it, a React app integrates by hand-writing the effect:
7
+ *
8
+ * useEffect(() => {
9
+ * window.Chatly?.init({ publicId });
10
+ * return () => window.Chatly?.destroy();
11
+ * }, [publicId]);
12
+ *
13
+ * That snippet has three bugs, and all three were in the version of it we
14
+ * published in our own docs. In StrictMode it boots twice (two sessions, a
15
+ * visibly flickering launcher). With a `theme={{…}}` literal in the deps it
16
+ * re-inits on every parent render, closing the visitor's open panel. And
17
+ * nothing in it ever calls `identify()`, so a visitor who signs in after load
18
+ * stays anonymous — because the only place to put an identity was `init`,
19
+ * which already ran.
20
+ *
21
+ * The lifecycle lives in `@chatlychat/widget-element` (refcounting, deferred
22
+ * teardown, structural prop comparison) so the React and custom-element
23
+ * wrappers cannot drift on the part that is hard.
24
+ *
25
+ * ── React is a peerDependency ───────────────────────────────────────────
26
+ *
27
+ * Declared `peerDependencies`, never `dependencies`. A real dependency would
28
+ * resolve a SECOND copy of React in any consumer whose version differs by so
29
+ * much as a patch, and two Reacts on one page break hooks with
30
+ * "Invalid hook call" — from a component the consumer did not write. A peer
31
+ * dep on the framework the consumer is already using is the one case where
32
+ * adding to the dependency block is right, and it costs their install nothing.
33
+ */
34
+ import { type ChatlyEmbedHandlers, type ChatlyEmbedOptions } from '@chatlychat/widget-element';
35
+ import type { CSSProperties, ReactElement } from 'react';
36
+ export interface ChatlyWidgetProps extends ChatlyEmbedOptions, ChatlyEmbedHandlers {
37
+ /**
38
+ * Render the chat INSIDE this component's own box instead of floating it in
39
+ * the page corner.
40
+ *
41
+ * This is the React-shaped answer to `mountTo`. The container is a `<div>`
42
+ * this component renders, so it provably exists when `init` runs — which is
43
+ * the failure the widget's `mountTo` refusal exists to make loud ("not
44
+ * mounted — mountTo selector matched nothing"). Give it a height through
45
+ * `className` or `style`; it has a 420px minimum so a container with none
46
+ * is still usable.
47
+ */
48
+ inline?: boolean;
49
+ /** Only meaningful with `inline`. */
50
+ className?: string;
51
+ /** Only meaningful with `inline`. */
52
+ style?: CSSProperties;
53
+ }
54
+ export declare function ChatlyWidget(props: ChatlyWidgetProps): ReactElement | null;
55
+ //# sourceMappingURL=ChatlyWidget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatlyWidget.d.ts","sourceRoot":"","sources":["../src/ChatlyWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAEzD,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB,EAAE,mBAAmB;IAChF;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAmDD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,GAAG,IAAI,CAqF1E"}
@@ -0,0 +1,159 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * `<ChatlyWidget />` — the widget as a React component.
4
+ *
5
+ * ── What this replaces ──────────────────────────────────────────────────
6
+ *
7
+ * Without it, a React app integrates by hand-writing the effect:
8
+ *
9
+ * useEffect(() => {
10
+ * window.Chatly?.init({ publicId });
11
+ * return () => window.Chatly?.destroy();
12
+ * }, [publicId]);
13
+ *
14
+ * That snippet has three bugs, and all three were in the version of it we
15
+ * published in our own docs. In StrictMode it boots twice (two sessions, a
16
+ * visibly flickering launcher). With a `theme={{…}}` literal in the deps it
17
+ * re-inits on every parent render, closing the visitor's open panel. And
18
+ * nothing in it ever calls `identify()`, so a visitor who signs in after load
19
+ * stays anonymous — because the only place to put an identity was `init`,
20
+ * which already ran.
21
+ *
22
+ * The lifecycle lives in `@chatlychat/widget-element` (refcounting, deferred
23
+ * teardown, structural prop comparison) so the React and custom-element
24
+ * wrappers cannot drift on the part that is hard.
25
+ *
26
+ * ── React is a peerDependency ───────────────────────────────────────────
27
+ *
28
+ * Declared `peerDependencies`, never `dependencies`. A real dependency would
29
+ * resolve a SECOND copy of React in any consumer whose version differs by so
30
+ * much as a patch, and two Reacts on one page break hooks with
31
+ * "Invalid hook call" — from a component the consumer did not write. A peer
32
+ * dep on the framework the consumer is already using is the one case where
33
+ * adding to the dependency block is right, and it costs their install nothing.
34
+ */
35
+ import { startWidget, } from '@chatlychat/widget-element';
36
+ import { useEffect, useRef } from 'react';
37
+ /**
38
+ * Keys that are this component's own, not the widget's.
39
+ *
40
+ * Listed so `optionsFrom` can strip them by name. Spreading the rest means a
41
+ * widget option added later needs no change here — the failure that costs is
42
+ * the reverse (a prop of ours leaking into `init`), which this prevents.
43
+ */
44
+ const LOCAL_PROPS = ['inline', 'className', 'style'];
45
+ const HANDLER_PROPS = [
46
+ 'onReady',
47
+ 'onOpen',
48
+ 'onClose',
49
+ 'onMessage',
50
+ 'onConversation',
51
+ 'onIdentified',
52
+ 'onReset',
53
+ 'onAction',
54
+ 'onError',
55
+ ];
56
+ function optionsFrom(props, mountTo) {
57
+ const options = {};
58
+ const skip = new Set([...LOCAL_PROPS, ...HANDLER_PROPS]);
59
+ for (const [key, value] of Object.entries(props)) {
60
+ if (!skip.has(key) && value !== undefined) {
61
+ options[key] = value;
62
+ }
63
+ }
64
+ if (mountTo !== undefined) {
65
+ options['mountTo'] = mountTo;
66
+ }
67
+ return options;
68
+ }
69
+ function handlersFrom(props) {
70
+ return {
71
+ onReady: props.onReady,
72
+ onOpen: props.onOpen,
73
+ onClose: props.onClose,
74
+ onMessage: props.onMessage,
75
+ onConversation: props.onConversation,
76
+ onIdentified: props.onIdentified,
77
+ onReset: props.onReset,
78
+ onAction: props.onAction,
79
+ onError: props.onError,
80
+ };
81
+ }
82
+ export function ChatlyWidget(props) {
83
+ const container = useRef(null);
84
+ const handle = useRef(null);
85
+ /*
86
+ * The latest callbacks, behind a stable indirection.
87
+ *
88
+ * `onMessage={(m) => …}` is a NEW function on every render. Handing it
89
+ * straight to the lifecycle would either re-register subscribers every
90
+ * commit or pin the first render's closure forever — the stale-callback bug
91
+ * that makes a wrapper feel haunted (the handler fires with props from
92
+ * three navigations ago). One stable object of forwarders that read this ref
93
+ * is the standard answer and the only one that is correct in both
94
+ * directions.
95
+ */
96
+ const latest = useRef(handlersFrom(props));
97
+ useEffect(() => {
98
+ latest.current = handlersFrom(props);
99
+ // No dependency array: this must run after EVERY commit, because that is
100
+ // what "latest" means. There is nothing to compare and nothing to skip.
101
+ });
102
+ const forwarders = useRef({
103
+ onReady: (session) => latest.current.onReady?.(session),
104
+ onOpen: () => latest.current.onOpen?.(),
105
+ onClose: () => latest.current.onClose?.(),
106
+ onMessage: (message) => latest.current.onMessage?.(message),
107
+ onConversation: (detail) => latest.current.onConversation?.(detail),
108
+ onIdentified: (session) => latest.current.onIdentified?.(session),
109
+ onReset: (detail) => latest.current.onReset?.(detail),
110
+ onAction: (detail) => latest.current.onAction?.(detail),
111
+ onError: (detail) => latest.current.onError?.(detail),
112
+ });
113
+ /*
114
+ * The props of the FIRST render, captured by `useRef`'s initial-value
115
+ * semantics rather than read from the render scope.
116
+ *
117
+ * The mount effect has an empty dependency array — mount and unmount, which
118
+ * is exactly right — and reading `props` inside it would make
119
+ * `react-hooks/exhaustive-deps` demand them as dependencies, at which point
120
+ * the effect re-runs on every render and we are back to re-initialising the
121
+ * widget for a new object literal. The update effect below is what applies
122
+ * later props, and it does so through a structural comparison.
123
+ */
124
+ const initial = useRef(props);
125
+ useEffect(() => {
126
+ if (initial.current.inline === true && container.current === null) {
127
+ /*
128
+ * Should be impossible — effects run after the DOM is committed — but if
129
+ * it ever happens the alternative is mounting a FLOATING widget on a
130
+ * page built to embed one, which is the exact outcome the widget's own
131
+ * refusal path exists to prevent. Refuse, and say which component.
132
+ */
133
+ console.warn('[Chatly] <ChatlyWidget inline> did not get its container — not mounted');
134
+ return undefined;
135
+ }
136
+ const started = startWidget(
137
+ // The claim identity. `forwarders` is a per-instance ref object, so two
138
+ // mounted components are two claims and one of them unmounting does not
139
+ // take the widget with it.
140
+ forwarders, optionsFrom(initial.current, container.current ?? undefined), forwarders.current);
141
+ handle.current = started;
142
+ return () => {
143
+ started.stop();
144
+ handle.current = null;
145
+ };
146
+ }, []);
147
+ useEffect(() => {
148
+ // Again no dependency array. Every commit re-offers the props; the
149
+ // lifecycle decides what, if anything, that means — `visitor` becomes an
150
+ // `identify()`, `locale` a `setLocale()`, and only a material change to
151
+ // the rest costs a re-init.
152
+ handle.current?.update(optionsFrom(props, container.current ?? undefined), forwarders.current);
153
+ });
154
+ if (props.inline !== true) {
155
+ return null;
156
+ }
157
+ return _jsx("div", { ref: container, className: props.className, style: props.style });
158
+ }
159
+ //# sourceMappingURL=ChatlyWidget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatlyWidget.js","sourceRoot":"","sources":["../src/ChatlyWidget.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `@chatlychat/widget-react` — the Chatly widget as a React component.
3
+ *
4
+ * import { ChatlyWidget, useChatly } from '@chatlychat/widget-react';
5
+ *
6
+ * function App() {
7
+ * return (
8
+ * <>
9
+ * <ChatlyWidget publicId="acme-web" visitor={{ externalId: user.id }} />
10
+ * <Routes />
11
+ * </>
12
+ * );
13
+ * }
14
+ *
15
+ * function HelpButton() {
16
+ * const chatly = useChatly();
17
+ * return <button onClick={() => chatly.open('help')}>Help</button>;
18
+ * }
19
+ *
20
+ * React is a peerDependency — see the note at the top of `ChatlyWidget.tsx`.
21
+ * The embed lifecycle comes from `@chatlychat/widget-element`, which has no
22
+ * dependencies of its own and no side effects on import; the custom element it
23
+ * also ships is NOT registered by anything here.
24
+ */
25
+ export { ChatlyWidget, type ChatlyWidgetProps } from './ChatlyWidget.js';
26
+ export { useChatly, useChatlyEvent } from './useChatly.js';
27
+ /**
28
+ * Re-exported so a consumer types their props from one import rather than
29
+ * reaching into the element package for a theme type.
30
+ */
31
+ export type { ChatlyBehavior, ChatlyCopy, ChatlyEmbedHandlers, ChatlyEmbedOptions, ChatlyEventName, ChatlyIdentity, ChatlyInitOptions, ChatlyMessageEvent, ChatlyQuickLink, ChatlyQuip, ChatlySession, ChatlyTheme, } from '@chatlychat/widget-element';
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;;GAGG;AACH,YAAY,EACV,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,UAAU,EACV,aAAa,EACb,WAAW,GACZ,MAAM,4BAA4B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `@chatlychat/widget-react` — the Chatly widget as a React component.
3
+ *
4
+ * import { ChatlyWidget, useChatly } from '@chatlychat/widget-react';
5
+ *
6
+ * function App() {
7
+ * return (
8
+ * <>
9
+ * <ChatlyWidget publicId="acme-web" visitor={{ externalId: user.id }} />
10
+ * <Routes />
11
+ * </>
12
+ * );
13
+ * }
14
+ *
15
+ * function HelpButton() {
16
+ * const chatly = useChatly();
17
+ * return <button onClick={() => chatly.open('help')}>Help</button>;
18
+ * }
19
+ *
20
+ * React is a peerDependency — see the note at the top of `ChatlyWidget.tsx`.
21
+ * The embed lifecycle comes from `@chatlychat/widget-element`, which has no
22
+ * dependencies of its own and no side effects on import; the custom element it
23
+ * also ships is NOT registered by anything here.
24
+ */
25
+ export { ChatlyWidget } from './ChatlyWidget.js';
26
+ export { useChatly, useChatlyEvent } from './useChatly.js';
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,YAAY,EAA0B,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Hooks for controlling a widget that is already mounted.
3
+ *
4
+ * The split matters: `<ChatlyWidget />` OWNS the widget (one per page, high in
5
+ * the tree), and everything else — a "Chat with us" button in the header, a
6
+ * badge in the nav, a help link inside a modal — controls it through
7
+ * `useChatly()`. Rendering a second `<ChatlyWidget />` to get at `open()` is
8
+ * the mistake this exists to make unnecessary, and the lifecycle warns when
9
+ * someone makes it anyway.
10
+ */
11
+ import { controls, type ChatlyEventName } from '@chatlychat/widget-element';
12
+ /**
13
+ * The widget's post-boot methods.
14
+ *
15
+ * A module-level constant, returned as-is: it is referentially stable, so it
16
+ * is safe in a dependency array and in a `useCallback` closure, and there is
17
+ * no state for a hook to hold. It is a hook rather than a plain import purely
18
+ * for discoverability — and because it is where the "mount the widget first"
19
+ * contract is documented.
20
+ *
21
+ * Every method returns `void`, including `identify`, which the widget's own
22
+ * API returns a promise from. The call may be queued before the bundle has
23
+ * arrived, and there is nothing to await in that state; `useChatlyEvent`
24
+ * (or `onIdentified`) is how you learn it landed.
25
+ *
26
+ * Calling any of these with no widget mounted warns in the console and does
27
+ * nothing. It does not throw: a click handler on the host page must not break
28
+ * because the chat is misconfigured.
29
+ */
30
+ export declare function useChatly(): typeof controls;
31
+ /**
32
+ * Subscribe to a widget event for the life of this component.
33
+ *
34
+ * The handler is read through a ref, so an inline arrow function does not
35
+ * re-subscribe on every render — the same reason `<ChatlyWidget />` keeps its
36
+ * callbacks behind one. Without it, a consumer writing the obvious
37
+ * `useChatlyEvent('message', (m) => setCount(c => c + 1))` would register and
38
+ * unregister a subscriber on every commit, and would do so against the
39
+ * widget's `off`, which can only remove a handler it matches by identity.
40
+ *
41
+ * `'ready'` fires immediately if the session is already up: the widget replays
42
+ * the last one, which is what makes this usable from a component that mounts
43
+ * long after the page did.
44
+ */
45
+ export declare function useChatlyEvent(event: ChatlyEventName, handler: (detail: unknown) => void): void;
46
+ //# sourceMappingURL=useChatly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useChatly.d.ts","sourceRoot":"","sources":["../src/useChatly.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAa,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGvF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,IAAI,OAAO,QAAQ,CAE3C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAU/F"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Hooks for controlling a widget that is already mounted.
3
+ *
4
+ * The split matters: `<ChatlyWidget />` OWNS the widget (one per page, high in
5
+ * the tree), and everything else — a "Chat with us" button in the header, a
6
+ * badge in the nav, a help link inside a modal — controls it through
7
+ * `useChatly()`. Rendering a second `<ChatlyWidget />` to get at `open()` is
8
+ * the mistake this exists to make unnecessary, and the lifecycle warns when
9
+ * someone makes it anyway.
10
+ */
11
+ import { controls, subscribe } from '@chatlychat/widget-element';
12
+ import { useEffect, useRef } from 'react';
13
+ /**
14
+ * The widget's post-boot methods.
15
+ *
16
+ * A module-level constant, returned as-is: it is referentially stable, so it
17
+ * is safe in a dependency array and in a `useCallback` closure, and there is
18
+ * no state for a hook to hold. It is a hook rather than a plain import purely
19
+ * for discoverability — and because it is where the "mount the widget first"
20
+ * contract is documented.
21
+ *
22
+ * Every method returns `void`, including `identify`, which the widget's own
23
+ * API returns a promise from. The call may be queued before the bundle has
24
+ * arrived, and there is nothing to await in that state; `useChatlyEvent`
25
+ * (or `onIdentified`) is how you learn it landed.
26
+ *
27
+ * Calling any of these with no widget mounted warns in the console and does
28
+ * nothing. It does not throw: a click handler on the host page must not break
29
+ * because the chat is misconfigured.
30
+ */
31
+ export function useChatly() {
32
+ return controls;
33
+ }
34
+ /**
35
+ * Subscribe to a widget event for the life of this component.
36
+ *
37
+ * The handler is read through a ref, so an inline arrow function does not
38
+ * re-subscribe on every render — the same reason `<ChatlyWidget />` keeps its
39
+ * callbacks behind one. Without it, a consumer writing the obvious
40
+ * `useChatlyEvent('message', (m) => setCount(c => c + 1))` would register and
41
+ * unregister a subscriber on every commit, and would do so against the
42
+ * widget's `off`, which can only remove a handler it matches by identity.
43
+ *
44
+ * `'ready'` fires immediately if the session is already up: the widget replays
45
+ * the last one, which is what makes this usable from a component that mounts
46
+ * long after the page did.
47
+ */
48
+ export function useChatlyEvent(event, handler) {
49
+ const latest = useRef(handler);
50
+ useEffect(() => {
51
+ latest.current = handler;
52
+ });
53
+ useEffect(() => {
54
+ return subscribe(event, (detail) => latest.current(detail));
55
+ // `event` only. The handler is deliberately NOT a dependency — see above.
56
+ }, [event]);
57
+ }
58
+ //# sourceMappingURL=useChatly.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useChatly.js","sourceRoot":"","sources":["../src/useChatly.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAwB,MAAM,4BAA4B,CAAC;AACvF,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,KAAsB,EAAE,OAAkC;IACvF,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,SAAS,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC5D,0EAA0E;IAC5E,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "@chatlychat/widget-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "The Chatly chat widget as a React component — mount, unmount, identity and locale changes handled, StrictMode-safe.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/livechat/platform/tree/main/packages/widget-react",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/livechat/platform.git",
10
+ "directory": "packages/widget-react"
11
+ },
12
+ "bugs": "https://github.com/livechat/platform/issues",
13
+ "keywords": [
14
+ "chatly",
15
+ "livechat",
16
+ "chat",
17
+ "widget",
18
+ "react",
19
+ "support"
20
+ ],
21
+ "author": "LiveChat Platform Engineering",
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "!dist/**/*.spec.*",
36
+ "README.md",
37
+ "LICENSE",
38
+ "CHANGELOG.md"
39
+ ],
40
+ "sideEffects": false,
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "prepublishOnly": "pnpm build",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit",
45
+ "lint": "eslint src --max-warnings=0",
46
+ "test": "vitest run"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "registry": "https://registry.npmjs.org/",
51
+ "provenance": true
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ },
56
+ "dependencies": {
57
+ "@chatlychat/widget-element": "workspace:*"
58
+ },
59
+ "peerDependencies": {
60
+ "react": "^18.0.0 || ^19.0.0"
61
+ },
62
+ "devDependencies": {
63
+ "@livechat/config": "workspace:*",
64
+ "@testing-library/dom": "^10.4.1",
65
+ "@testing-library/react": "^16.3.2",
66
+ "@types/react": "catalog:",
67
+ "@types/react-dom": "catalog:",
68
+ "jsdom": "^30.0.1",
69
+ "react": "catalog:",
70
+ "react-dom": "catalog:",
71
+ "typescript": "catalog:",
72
+ "vitest": "catalog:"
73
+ }
74
+ }