@userz-ai/react 1.0.1 → 2.0.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/README.md +2 -2
- package/dist/index.d.mts +9 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +26 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ Idiomatic React bindings for the [Userz](https://userz.ai) feedback widget. A pr
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
pnpm add @userz-ai/react @userz-ai/browser
|
|
9
|
-
# Optional peer for richer screenshots
|
|
10
|
-
pnpm add @zumer/snapdom
|
|
11
9
|
```
|
|
12
10
|
|
|
11
|
+
Screenshot capture (`modern-screenshot`) ships as a transitive dependency of `@userz-ai/browser` — no extra install step required.
|
|
12
|
+
|
|
13
13
|
React 18 and 19 are both supported.
|
|
14
14
|
|
|
15
15
|
## Quick start
|
package/dist/index.d.mts
CHANGED
|
@@ -11,6 +11,11 @@ interface UserzProviderProps extends UserzConfig {
|
|
|
11
11
|
* component tree. Re-renders DON'T tear down the widget — config changes
|
|
12
12
|
* after the first render are ignored (same as Sentry/Datadog SDKs). Use
|
|
13
13
|
* `useUserz()` to call methods (setUser, submit, …) imperatively.
|
|
14
|
+
*
|
|
15
|
+
* StrictMode dev lifecycle is `render → setup → cleanup → setup` (render
|
|
16
|
+
* only fires ONCE per mount cycle). The cleanup destroys the widget and
|
|
17
|
+
* nulls the ref; the second setup re-creates it and forces a re-render so
|
|
18
|
+
* the context value swaps from the destroyed instance to the live one.
|
|
14
19
|
*/
|
|
15
20
|
declare function UserzProvider({
|
|
16
21
|
children,
|
|
@@ -54,6 +59,10 @@ interface ScreenshotEditorProps {
|
|
|
54
59
|
brandColor?: string;
|
|
55
60
|
/** Save-button text color. Defaults to brand foreground. */
|
|
56
61
|
brandForeground?: string;
|
|
62
|
+
/** Cancel-button surface color. Defaults to white. */
|
|
63
|
+
backgroundColor?: string;
|
|
64
|
+
/** Cancel-button text color. Defaults to near-black. */
|
|
65
|
+
textColor?: string;
|
|
57
66
|
}
|
|
58
67
|
declare function ScreenshotEditor(props: ScreenshotEditorProps): ReactNode;
|
|
59
68
|
//#endregion
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.tsx","../src/ScreenshotEditor.tsx","../src/UserzTarget.tsx"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.tsx","../src/ScreenshotEditor.tsx","../src/UserzTarget.tsx"],"mappings":";;;;;UAYiB,kBAAA,SAA2B,WAAA;EAC1C,QAAA,EAAU,SAAA;AAAA;AADZ;;;;;;;;;AAeA;;AAfA,iBAegB,aAAA,CAAA;EAAgB,QAAA;EAAA,GAAa;AAAA,GAAU,kBAAA,GAAkB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,iBA0CzD,QAAA,CAAA,GAAY,KAAA;;;;;;;AAzD5B;;;;;;;;;AAeA;;;;;;UCGiB,qBAAA;EDHwD;ECKvE,IAAA,EAAM,IAAA;EDLwB;ECO9B,KAAA;EACA,MAAA;EDRuE;ECUvE,MAAA,GAAS,SAAA,EAAW,IAAA,YAAgB,OAAA;EACpC,QAAA;EDXuE;ECavE,KAAA,GAAQ,aAAA;EACR,SAAA;;EAEA,UAAA;ED0B+B;ECxB/B,eAAA;;EAEA,eAAA;EAjBe;EAmBf,SAAA;AAAA;AAAA,iBAGc,gBAAA,CAAiB,KAAA,EAAO,qBAAA,GAAwB,SAAA;;;UCxC/C,gBAAA;;;EAGf,IAAA;EFHe;EEKf,IAAA,GAAO,MAAA;;;EAGP,QAAA,EAAU,SAAA;AAAA;;;;AFOZ;;;;;;;;iBEOgB,WAAA,CAAA;EAAc,IAAA;EAAM,IAAA;EAAM;AAAA,GAAY,gBAAA,GAAgB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UZ_DEFAULT_BRAND, UZ_DEFAULT_BRAND_FG, createUserz } from "@userz-ai/browser";
|
|
2
|
-
import { Children, Suspense, cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Children, Suspense, cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/provider.tsx
|
|
@@ -9,21 +9,33 @@ const UserzCtx = createContext(null);
|
|
|
9
9
|
* component tree. Re-renders DON'T tear down the widget — config changes
|
|
10
10
|
* after the first render are ignored (same as Sentry/Datadog SDKs). Use
|
|
11
11
|
* `useUserz()` to call methods (setUser, submit, …) imperatively.
|
|
12
|
+
*
|
|
13
|
+
* StrictMode dev lifecycle is `render → setup → cleanup → setup` (render
|
|
14
|
+
* only fires ONCE per mount cycle). The cleanup destroys the widget and
|
|
15
|
+
* nulls the ref; the second setup re-creates it and forces a re-render so
|
|
16
|
+
* the context value swaps from the destroyed instance to the live one.
|
|
12
17
|
*/
|
|
13
18
|
function UserzProvider({ children, ...config }) {
|
|
14
19
|
const instance = useRef(null);
|
|
15
|
-
|
|
20
|
+
const configRef = useRef(config);
|
|
21
|
+
const [, rerender] = useReducer((x) => x + 1, 0);
|
|
22
|
+
if (instance.current === null && typeof document !== "undefined") instance.current = createUserz(configRef.current);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (instance.current === null && typeof document !== "undefined") {
|
|
25
|
+
instance.current = createUserz(configRef.current);
|
|
26
|
+
rerender();
|
|
27
|
+
}
|
|
28
|
+
return () => {
|
|
29
|
+
instance.current?.destroy();
|
|
30
|
+
instance.current = null;
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
16
33
|
const initialUser = config.initialUser;
|
|
17
34
|
useEffect(() => {
|
|
18
35
|
if (initialUser !== void 0) instance.current?.setUser(initialUser);
|
|
19
36
|
}, [initialUser]);
|
|
20
|
-
useEffect(() => () => {
|
|
21
|
-
instance.current?.destroy();
|
|
22
|
-
instance.current = null;
|
|
23
|
-
}, []);
|
|
24
|
-
const value = useMemo(() => instance.current, []);
|
|
25
37
|
return /* @__PURE__ */ jsx(UserzCtx.Provider, {
|
|
26
|
-
value,
|
|
38
|
+
value: instance.current,
|
|
27
39
|
children
|
|
28
40
|
});
|
|
29
41
|
}
|
|
@@ -70,7 +82,7 @@ async function loadTldraw() {
|
|
|
70
82
|
return null;
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
|
-
function TldrawEditor({ tldraw, blob, width, height, onSave, onCancel, style, className, brandColor = UZ_DEFAULT_BRAND, brandForeground = UZ_DEFAULT_BRAND_FG }) {
|
|
85
|
+
function TldrawEditor({ tldraw, blob, width, height, onSave, onCancel, style, className, brandColor = UZ_DEFAULT_BRAND, brandForeground = UZ_DEFAULT_BRAND_FG, backgroundColor = "white", textColor = "#111" }) {
|
|
74
86
|
const [editor, setEditor] = useState(null);
|
|
75
87
|
const [saving, setSaving] = useState(false);
|
|
76
88
|
const onMount = useCallback(async (ed) => {
|
|
@@ -154,13 +166,13 @@ function TldrawEditor({ tldraw, blob, width, height, onSave, onCancel, style, cl
|
|
|
154
166
|
children: [onCancel && /* @__PURE__ */ jsx("button", {
|
|
155
167
|
type: "button",
|
|
156
168
|
onClick: onCancel,
|
|
157
|
-
style: editorButtonStyle("ghost", brandColor, brandForeground),
|
|
169
|
+
style: editorButtonStyle("ghost", brandColor, brandForeground, backgroundColor, textColor),
|
|
158
170
|
disabled: saving,
|
|
159
171
|
children: "Cancel"
|
|
160
172
|
}), /* @__PURE__ */ jsx("button", {
|
|
161
173
|
type: "button",
|
|
162
174
|
onClick: handleSave,
|
|
163
|
-
style: editorButtonStyle("primary", brandColor, brandForeground),
|
|
175
|
+
style: editorButtonStyle("primary", brandColor, brandForeground, backgroundColor, textColor),
|
|
164
176
|
disabled: saving || !editor,
|
|
165
177
|
children: saving ? "Saving…" : "Save annotation"
|
|
166
178
|
})]
|
|
@@ -219,7 +231,7 @@ const loadingStyle = {
|
|
|
219
231
|
fontSize: 14,
|
|
220
232
|
color: "#555"
|
|
221
233
|
};
|
|
222
|
-
function editorButtonStyle(kind, brandColor, brandForeground) {
|
|
234
|
+
function editorButtonStyle(kind, brandColor, brandForeground, backgroundColor, textColor) {
|
|
223
235
|
if (kind === "primary") return {
|
|
224
236
|
padding: "8px 14px",
|
|
225
237
|
borderRadius: 6,
|
|
@@ -234,8 +246,8 @@ function editorButtonStyle(kind, brandColor, brandForeground) {
|
|
|
234
246
|
padding: "8px 14px",
|
|
235
247
|
borderRadius: 6,
|
|
236
248
|
border: "1px solid rgba(0,0,0,0.18)",
|
|
237
|
-
background:
|
|
238
|
-
color:
|
|
249
|
+
background: backgroundColor,
|
|
250
|
+
color: textColor,
|
|
239
251
|
cursor: "pointer"
|
|
240
252
|
};
|
|
241
253
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/provider.tsx","../src/ScreenshotEditor.tsx","../src/UserzTarget.tsx"],"sourcesContent":["import { createUserz, type Userz, type UserzConfig } from '@userz-ai/browser';\nimport { createContext, type ReactNode, useContext, useEffect, useMemo, useRef } from 'react';\n\nconst UserzCtx = createContext<Userz | null>(null);\n\nexport interface UserzProviderProps extends UserzConfig {\n children: ReactNode;\n}\n\n/**\n * React provider that owns one Userz instance for the lifetime of the\n * component tree. Re-renders DON'T tear down the widget — config changes\n * after the first render are ignored (same as Sentry/Datadog SDKs). Use\n * `useUserz()` to call methods (setUser, submit, …) imperatively.\n */\nexport function UserzProvider({ children, ...config }: UserzProviderProps) {\n const instance = useRef<Userz | null>(null);\n\n // Initialize once, lazily — and only in the browser. Next.js, Remix, and\n // any other RSC/SSR framework will render this provider on the server to\n // emit the initial HTML; `createUserz` touches `document` immediately so\n // running it server-side crashes the render. The widget has nothing to do\n // during SSR anyway (no bubble to paint, no console to capture), so we\n // defer construction to the client pass.\n if (instance.current === null && typeof document !== 'undefined') {\n instance.current = createUserz(config);\n }\n\n // Pass through identity/metadata updates if the parent passes them via prop\n // changes after mount.\n const initialUser = config.initialUser;\n useEffect(() => {\n if (initialUser !== undefined) instance.current?.setUser(initialUser);\n }, [initialUser]);\n\n useEffect(\n () => () => {\n instance.current?.destroy();\n instance.current = null;\n },\n [],\n );\n\n const value = useMemo(() => instance.current, []);\n return <UserzCtx.Provider value={value}>{children}</UserzCtx.Provider>;\n}\n\nexport function useUserz(): Userz {\n const u = useContext(UserzCtx);\n if (!u) throw new Error('useUserz must be used inside <UserzProvider>');\n return u;\n}\n","import { UZ_DEFAULT_BRAND, UZ_DEFAULT_BRAND_FG } from '@userz-ai/browser';\nimport {\n type CSSProperties,\n type ReactNode,\n Suspense,\n useCallback,\n useEffect,\n useState,\n} from 'react';\n\n/**\n * Annotated screenshot editor.\n *\n * Wraps tldraw to give the end-user a quick \"circle the broken thing, type\n * an arrow, save\" workflow before submitting feedback. The tldraw bundle\n * (~600KB) is dynamic-imported on first mount so apps that never open the\n * editor don't pay for it; the component shows a loading state until ready.\n *\n * tldraw is declared as an OPTIONAL peer dep in package.json. Apps that\n * want this component MUST install `tldraw` themselves; without it the\n * component renders an instructive fallback instead of throwing.\n *\n * Usage:\n * <ScreenshotEditor\n * blob={blob}\n * onSave={(annotated) => userz.submit({ ..., attachments: [annotated] })}\n * onCancel={() => setOpen(false)}\n * />\n */\n\nexport interface ScreenshotEditorProps {\n /** PNG/JPEG blob to seed the canvas with. */\n blob: Blob;\n /** Optional dimensions override; default is the blob's natural size. */\n width?: number;\n height?: number;\n /** Receives the annotated PNG. */\n onSave: (annotated: Blob) => void | Promise<void>;\n onCancel?: () => void;\n /** Inline style override on the editor container. */\n style?: CSSProperties;\n className?: string;\n /** Save-button color. Defaults to the Userz brand mint. */\n brandColor?: string;\n /** Save-button text color. Defaults to brand foreground. */\n brandForeground?: string;\n}\n\nexport function ScreenshotEditor(props: ScreenshotEditorProps): ReactNode {\n return (\n <Suspense fallback={<EditorLoadingState />}>\n <LazyEditor {...props} />\n </Suspense>\n );\n}\n\nfunction LazyEditor(props: ScreenshotEditorProps) {\n const [tldraw, setTldraw] = useState<TldrawModule | null | 'missing'>(null);\n\n useEffect(() => {\n let cancelled = false;\n loadTldraw().then(\n (mod) => {\n if (!cancelled) setTldraw(mod ?? 'missing');\n },\n () => {\n if (!cancelled) setTldraw('missing');\n },\n );\n return () => {\n cancelled = true;\n };\n }, []);\n\n if (tldraw === null) return <EditorLoadingState />;\n if (tldraw === 'missing') return <MissingPeerFallback />;\n\n return <TldrawEditor tldraw={tldraw} {...props} />;\n}\n\ninterface TldrawModule {\n Tldraw: (props: Record<string, unknown>) => ReactNode;\n exportAs: (\n editor: unknown,\n shapeIds: unknown[],\n format: string,\n opts?: Record<string, unknown>,\n ) => Promise<Blob>;\n AssetRecordType: {\n createId(): string;\n };\n createShapeId(): string;\n}\n\nasync function loadTldraw(): Promise<TldrawModule | null> {\n try {\n // The string is split with a runtime expression so bundlers don't try\n // to follow the import; tldraw stays out of the build graph entirely\n // for apps that don't install it.\n const name = ['tld', 'raw'].join('');\n const mod = (await import(/* @vite-ignore */ name)) as Partial<TldrawModule>;\n if (!mod.Tldraw) return null;\n return mod as TldrawModule;\n } catch {\n return null;\n }\n}\n\ninterface TldrawEditorProps extends ScreenshotEditorProps {\n tldraw: TldrawModule;\n}\n\nfunction TldrawEditor({\n tldraw,\n blob,\n width,\n height,\n onSave,\n onCancel,\n style,\n className,\n brandColor = UZ_DEFAULT_BRAND,\n brandForeground = UZ_DEFAULT_BRAND_FG,\n}: TldrawEditorProps) {\n const [editor, setEditor] = useState<unknown>(null);\n const [saving, setSaving] = useState(false);\n\n const onMount = useCallback(\n async (ed: unknown) => {\n setEditor(ed);\n const url = URL.createObjectURL(blob);\n try {\n const dims = width && height ? { w: width, h: height } : await measureBlob(blob);\n const assetId = tldraw.AssetRecordType.createId();\n const shapeId = tldraw.createShapeId();\n const editorAny = ed as {\n createAssets: (assets: unknown[]) => void;\n createShape: (s: unknown) => void;\n };\n editorAny.createAssets([\n {\n id: assetId,\n type: 'image',\n typeName: 'asset',\n props: {\n name: 'screenshot.png',\n src: url,\n w: dims.w,\n h: dims.h,\n mimeType: blob.type || 'image/png',\n isAnimated: false,\n },\n meta: {},\n },\n ]);\n editorAny.createShape({\n id: shapeId,\n type: 'image',\n x: 0,\n y: 0,\n props: { assetId, w: dims.w, h: dims.h },\n });\n } catch {\n // best-effort seeding; user can still draw on a blank canvas\n }\n },\n [tldraw, blob, width, height],\n );\n\n const handleSave = useCallback(async () => {\n if (!editor || saving) return;\n setSaving(true);\n try {\n const editorAny = editor as { getCurrentPageShapeIds(): Set<unknown> };\n const ids = Array.from(editorAny.getCurrentPageShapeIds());\n const png = await tldraw.exportAs(editor, ids, 'png', { background: false });\n await onSave(png);\n } finally {\n setSaving(false);\n }\n }, [editor, saving, tldraw, onSave]);\n\n const TldrawCmp = tldraw.Tldraw as unknown as (p: {\n onMount: (ed: unknown) => void;\n persistenceKey?: string;\n }) => ReactNode;\n\n return (\n <div\n className={className}\n style={{\n position: 'relative',\n width: '100%',\n height: '60vh',\n minHeight: 360,\n ...style,\n }}\n >\n <TldrawCmp onMount={onMount} />\n <div\n style={{\n position: 'absolute',\n right: 12,\n bottom: 12,\n display: 'flex',\n gap: 8,\n zIndex: 1000,\n }}\n >\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n style={editorButtonStyle('ghost', brandColor, brandForeground)}\n disabled={saving}\n >\n Cancel\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleSave}\n style={editorButtonStyle('primary', brandColor, brandForeground)}\n disabled={saving || !editor}\n >\n {saving ? 'Saving…' : 'Save annotation'}\n </button>\n </div>\n </div>\n );\n}\n\nfunction EditorLoadingState() {\n return (\n <div style={loadingStyle}>\n <span>Loading editor…</span>\n </div>\n );\n}\n\nfunction MissingPeerFallback() {\n return (\n <div style={loadingStyle}>\n <div style={{ maxWidth: 360, textAlign: 'center', lineHeight: 1.45 }}>\n <strong>Screenshot annotation unavailable.</strong>\n <div style={{ marginTop: 8, fontSize: 13, opacity: 0.8 }}>\n Install <code>tldraw</code> in your app to enable the in-widget editor:\n <pre\n style={{\n marginTop: 8,\n padding: 8,\n background: 'rgba(0,0,0,0.05)',\n borderRadius: 6,\n fontSize: 12,\n }}\n >\n pnpm add tldraw\n </pre>\n </div>\n </div>\n </div>\n );\n}\n\nconst loadingStyle: CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '100%',\n height: '60vh',\n minHeight: 360,\n border: '1px dashed rgba(0,0,0,0.15)',\n borderRadius: 8,\n fontSize: 14,\n color: '#555',\n};\n\nfunction editorButtonStyle(\n kind: 'primary' | 'ghost',\n brandColor: string,\n brandForeground: string,\n): CSSProperties {\n if (kind === 'primary') {\n return {\n padding: '8px 14px',\n borderRadius: 6,\n border: 'none',\n background: brandColor,\n color: brandForeground,\n fontWeight: 600,\n cursor: 'pointer',\n boxShadow: '0 4px 12px rgba(0,0,0,0.18)',\n };\n }\n return {\n padding: '8px 14px',\n borderRadius: 6,\n border: '1px solid rgba(0,0,0,0.18)',\n background: 'white',\n color: '#111',\n cursor: 'pointer',\n };\n}\n\nasync function measureBlob(blob: Blob): Promise<{ w: number; h: number }> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n const url = URL.createObjectURL(blob);\n img.onload = () => {\n URL.revokeObjectURL(url);\n resolve({ w: img.naturalWidth, h: img.naturalHeight });\n };\n img.onerror = (err) => {\n URL.revokeObjectURL(url);\n reject(err);\n };\n img.src = url;\n });\n}\n","import {\n Children,\n cloneElement,\n isValidElement,\n type ReactElement,\n type ReactNode,\n useEffect,\n useMemo,\n useRef,\n} from 'react';\nimport { useUserz } from './provider';\n\nexport interface UserzTargetProps {\n /** Stable display name for this target. Surfaces in feedback as the\n * \"component the user clicked on\" label. */\n name: string;\n /** Opt-in metadata you want forwarded with feedback. Keep small + non-sensitive. */\n meta?: Record<string, unknown>;\n /** A single child element (no fragments). We attach a ref to it without\n * a wrapper div so layout is unchanged. */\n children: ReactNode;\n}\n\n/**\n * Wraps a child element to mark it as a \"feedback target\" the end-user can\n * click on while the targeting overlay is active (Ctrl+Shift+U by default).\n *\n * Implementation: clones the single child, attaches a ref, and registers/\n * unregisters with the Userz instance over the child's lifecycle. We do NOT\n * inject a wrapper div — that would break flex/grid layouts. As a result,\n * the child must accept a `ref`; for most DOM elements and `forwardRef`\n * components this is fine. For function components without forwardRef, we\n * fall back to a `display: contents` wrapper.\n */\nexport function UserzTarget({ name, meta, children }: UserzTargetProps) {\n const u = useUserz();\n const ref = useRef<HTMLElement | null>(null);\n // Stable meta identity so the effect only re-runs when shape actually changes.\n // The dep is intentionally the JSON string, not `meta`, so callers passing a\n // fresh object literal each render don't thrash the registration.\n const metaJson = JSON.stringify(meta ?? null);\n // biome-ignore lint/correctness/useExhaustiveDependencies: see above\n const stableMeta = useMemo(() => meta, [metaJson]);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n return u.registerTarget({ el, name, meta: stableMeta });\n }, [u, name, stableMeta]);\n\n const only = Children.only(children);\n if (isValidElement(only) && (typeof only.type === 'string' || hasForwardedRef(only))) {\n const original = (only.props as { ref?: unknown }).ref;\n return cloneElement(only as ReactElement<{ ref?: unknown }>, {\n ref: composeRefs(ref, original),\n });\n }\n // Function component without forwardRef — use a `display: contents` span\n // so we can attach a ref without affecting layout.\n return (\n <span\n ref={(el) => {\n ref.current = el;\n }}\n style={{ display: 'contents' }}\n >\n {only}\n </span>\n );\n}\n\nfunction hasForwardedRef(el: ReactElement): boolean {\n // Conservative heuristic — React.forwardRef components have a \"$$typeof\"\n // marker on their type. We can't import it portably across React versions,\n // so we check the symbol indirectly.\n const t = el.type as unknown as { $$typeof?: symbol };\n return typeof t === 'object' && typeof t.$$typeof?.toString === 'function';\n}\n\nfunction composeRefs<T>(...refs: unknown[]): (node: T) => void {\n return (node: T) => {\n for (const r of refs) {\n if (typeof r === 'function') (r as (n: T) => void)(node);\n else if (r && typeof r === 'object' && 'current' in r) {\n (r as { current: T }).current = node;\n }\n }\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/provider.tsx","../src/ScreenshotEditor.tsx","../src/UserzTarget.tsx"],"sourcesContent":["import { createUserz, type Userz, type UserzConfig } from '@userz-ai/browser';\nimport {\n createContext,\n type ReactNode,\n useContext,\n useEffect,\n useReducer,\n useRef,\n} from 'react';\n\nconst UserzCtx = createContext<Userz | null>(null);\n\nexport interface UserzProviderProps extends UserzConfig {\n children: ReactNode;\n}\n\n/**\n * React provider that owns one Userz instance for the lifetime of the\n * component tree. Re-renders DON'T tear down the widget — config changes\n * after the first render are ignored (same as Sentry/Datadog SDKs). Use\n * `useUserz()` to call methods (setUser, submit, …) imperatively.\n *\n * StrictMode dev lifecycle is `render → setup → cleanup → setup` (render\n * only fires ONCE per mount cycle). The cleanup destroys the widget and\n * nulls the ref; the second setup re-creates it and forces a re-render so\n * the context value swaps from the destroyed instance to the live one.\n */\nexport function UserzProvider({ children, ...config }: UserzProviderProps) {\n const instance = useRef<Userz | null>(null);\n // Freeze initial config — matches the documented \"ignored after mount\"\n // semantics and gives the StrictMode re-setup something to recreate from.\n const configRef = useRef(config);\n const [, rerender] = useReducer((x: number) => x + 1, 0);\n\n // Initialize once, lazily — and only in the browser. Next.js, Remix, and\n // any other RSC/SSR framework will render this provider on the server to\n // emit the initial HTML; `createUserz` touches `document` immediately so\n // running it server-side crashes the render. The widget has nothing to do\n // during SSR anyway (no bubble to paint, no console to capture), so we\n // defer construction to the client pass.\n if (instance.current === null && typeof document !== 'undefined') {\n instance.current = createUserz(configRef.current);\n }\n\n useEffect(() => {\n // StrictMode remount: the previous cleanup destroyed the widget and\n // nulled the ref, but render doesn't re-fire between cleanup and the\n // next setup. Recreate here, then force a re-render so the context\n // value (currently the destroyed instance) updates for children.\n if (instance.current === null && typeof document !== 'undefined') {\n instance.current = createUserz(configRef.current);\n rerender();\n }\n return () => {\n instance.current?.destroy();\n instance.current = null;\n };\n }, []);\n\n // Pass through identity updates if the parent passes them via prop\n // changes after mount.\n const initialUser = config.initialUser;\n useEffect(() => {\n if (initialUser !== undefined) instance.current?.setUser(initialUser);\n }, [initialUser]);\n\n return <UserzCtx.Provider value={instance.current}>{children}</UserzCtx.Provider>;\n}\n\nexport function useUserz(): Userz {\n const u = useContext(UserzCtx);\n if (!u) throw new Error('useUserz must be used inside <UserzProvider>');\n return u;\n}\n","import { UZ_DEFAULT_BRAND, UZ_DEFAULT_BRAND_FG } from '@userz-ai/browser';\nimport {\n type CSSProperties,\n type ReactNode,\n Suspense,\n useCallback,\n useEffect,\n useState,\n} from 'react';\n\n/**\n * Annotated screenshot editor.\n *\n * Wraps tldraw to give the end-user a quick \"circle the broken thing, type\n * an arrow, save\" workflow before submitting feedback. The tldraw bundle\n * (~600KB) is dynamic-imported on first mount so apps that never open the\n * editor don't pay for it; the component shows a loading state until ready.\n *\n * tldraw is declared as an OPTIONAL peer dep in package.json. Apps that\n * want this component MUST install `tldraw` themselves; without it the\n * component renders an instructive fallback instead of throwing.\n *\n * Usage:\n * <ScreenshotEditor\n * blob={blob}\n * onSave={(annotated) => userz.submit({ ..., attachments: [annotated] })}\n * onCancel={() => setOpen(false)}\n * />\n */\n\nexport interface ScreenshotEditorProps {\n /** PNG/JPEG blob to seed the canvas with. */\n blob: Blob;\n /** Optional dimensions override; default is the blob's natural size. */\n width?: number;\n height?: number;\n /** Receives the annotated PNG. */\n onSave: (annotated: Blob) => void | Promise<void>;\n onCancel?: () => void;\n /** Inline style override on the editor container. */\n style?: CSSProperties;\n className?: string;\n /** Save-button color. Defaults to the Userz brand mint. */\n brandColor?: string;\n /** Save-button text color. Defaults to brand foreground. */\n brandForeground?: string;\n /** Cancel-button surface color. Defaults to white. */\n backgroundColor?: string;\n /** Cancel-button text color. Defaults to near-black. */\n textColor?: string;\n}\n\nexport function ScreenshotEditor(props: ScreenshotEditorProps): ReactNode {\n return (\n <Suspense fallback={<EditorLoadingState />}>\n <LazyEditor {...props} />\n </Suspense>\n );\n}\n\nfunction LazyEditor(props: ScreenshotEditorProps) {\n const [tldraw, setTldraw] = useState<TldrawModule | null | 'missing'>(null);\n\n useEffect(() => {\n let cancelled = false;\n loadTldraw().then(\n (mod) => {\n if (!cancelled) setTldraw(mod ?? 'missing');\n },\n () => {\n if (!cancelled) setTldraw('missing');\n },\n );\n return () => {\n cancelled = true;\n };\n }, []);\n\n if (tldraw === null) return <EditorLoadingState />;\n if (tldraw === 'missing') return <MissingPeerFallback />;\n\n return <TldrawEditor tldraw={tldraw} {...props} />;\n}\n\ninterface TldrawModule {\n Tldraw: (props: Record<string, unknown>) => ReactNode;\n exportAs: (\n editor: unknown,\n shapeIds: unknown[],\n format: string,\n opts?: Record<string, unknown>,\n ) => Promise<Blob>;\n AssetRecordType: {\n createId(): string;\n };\n createShapeId(): string;\n}\n\nasync function loadTldraw(): Promise<TldrawModule | null> {\n try {\n // The string is split with a runtime expression so bundlers don't try\n // to follow the import; tldraw stays out of the build graph entirely\n // for apps that don't install it.\n const name = ['tld', 'raw'].join('');\n const mod = (await import(/* @vite-ignore */ name)) as Partial<TldrawModule>;\n if (!mod.Tldraw) return null;\n return mod as TldrawModule;\n } catch {\n return null;\n }\n}\n\ninterface TldrawEditorProps extends ScreenshotEditorProps {\n tldraw: TldrawModule;\n}\n\nfunction TldrawEditor({\n tldraw,\n blob,\n width,\n height,\n onSave,\n onCancel,\n style,\n className,\n brandColor = UZ_DEFAULT_BRAND,\n brandForeground = UZ_DEFAULT_BRAND_FG,\n backgroundColor = 'white',\n textColor = '#111',\n}: TldrawEditorProps) {\n const [editor, setEditor] = useState<unknown>(null);\n const [saving, setSaving] = useState(false);\n\n const onMount = useCallback(\n async (ed: unknown) => {\n setEditor(ed);\n const url = URL.createObjectURL(blob);\n try {\n const dims = width && height ? { w: width, h: height } : await measureBlob(blob);\n const assetId = tldraw.AssetRecordType.createId();\n const shapeId = tldraw.createShapeId();\n const editorAny = ed as {\n createAssets: (assets: unknown[]) => void;\n createShape: (s: unknown) => void;\n };\n editorAny.createAssets([\n {\n id: assetId,\n type: 'image',\n typeName: 'asset',\n props: {\n name: 'screenshot.png',\n src: url,\n w: dims.w,\n h: dims.h,\n mimeType: blob.type || 'image/png',\n isAnimated: false,\n },\n meta: {},\n },\n ]);\n editorAny.createShape({\n id: shapeId,\n type: 'image',\n x: 0,\n y: 0,\n props: { assetId, w: dims.w, h: dims.h },\n });\n } catch {\n // best-effort seeding; user can still draw on a blank canvas\n }\n },\n [tldraw, blob, width, height],\n );\n\n const handleSave = useCallback(async () => {\n if (!editor || saving) return;\n setSaving(true);\n try {\n const editorAny = editor as { getCurrentPageShapeIds(): Set<unknown> };\n const ids = Array.from(editorAny.getCurrentPageShapeIds());\n const png = await tldraw.exportAs(editor, ids, 'png', { background: false });\n await onSave(png);\n } finally {\n setSaving(false);\n }\n }, [editor, saving, tldraw, onSave]);\n\n const TldrawCmp = tldraw.Tldraw as unknown as (p: {\n onMount: (ed: unknown) => void;\n persistenceKey?: string;\n }) => ReactNode;\n\n return (\n <div\n className={className}\n style={{\n position: 'relative',\n width: '100%',\n height: '60vh',\n minHeight: 360,\n ...style,\n }}\n >\n <TldrawCmp onMount={onMount} />\n <div\n style={{\n position: 'absolute',\n right: 12,\n bottom: 12,\n display: 'flex',\n gap: 8,\n zIndex: 1000,\n }}\n >\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n style={editorButtonStyle('ghost', brandColor, brandForeground, backgroundColor, textColor)}\n disabled={saving}\n >\n Cancel\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleSave}\n style={editorButtonStyle('primary', brandColor, brandForeground, backgroundColor, textColor)}\n disabled={saving || !editor}\n >\n {saving ? 'Saving…' : 'Save annotation'}\n </button>\n </div>\n </div>\n );\n}\n\nfunction EditorLoadingState() {\n return (\n <div style={loadingStyle}>\n <span>Loading editor…</span>\n </div>\n );\n}\n\nfunction MissingPeerFallback() {\n return (\n <div style={loadingStyle}>\n <div style={{ maxWidth: 360, textAlign: 'center', lineHeight: 1.45 }}>\n <strong>Screenshot annotation unavailable.</strong>\n <div style={{ marginTop: 8, fontSize: 13, opacity: 0.8 }}>\n Install <code>tldraw</code> in your app to enable the in-widget editor:\n <pre\n style={{\n marginTop: 8,\n padding: 8,\n background: 'rgba(0,0,0,0.05)',\n borderRadius: 6,\n fontSize: 12,\n }}\n >\n pnpm add tldraw\n </pre>\n </div>\n </div>\n </div>\n );\n}\n\nconst loadingStyle: CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '100%',\n height: '60vh',\n minHeight: 360,\n border: '1px dashed rgba(0,0,0,0.15)',\n borderRadius: 8,\n fontSize: 14,\n color: '#555',\n};\n\nfunction editorButtonStyle(\n kind: 'primary' | 'ghost',\n brandColor: string,\n brandForeground: string,\n backgroundColor: string,\n textColor: string,\n): CSSProperties {\n if (kind === 'primary') {\n return {\n padding: '8px 14px',\n borderRadius: 6,\n border: 'none',\n background: brandColor,\n color: brandForeground,\n fontWeight: 600,\n cursor: 'pointer',\n boxShadow: '0 4px 12px rgba(0,0,0,0.18)',\n };\n }\n return {\n padding: '8px 14px',\n borderRadius: 6,\n border: '1px solid rgba(0,0,0,0.18)',\n background: backgroundColor,\n color: textColor,\n cursor: 'pointer',\n };\n}\n\nasync function measureBlob(blob: Blob): Promise<{ w: number; h: number }> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n const url = URL.createObjectURL(blob);\n img.onload = () => {\n URL.revokeObjectURL(url);\n resolve({ w: img.naturalWidth, h: img.naturalHeight });\n };\n img.onerror = (err) => {\n URL.revokeObjectURL(url);\n reject(err);\n };\n img.src = url;\n });\n}\n","import {\n Children,\n cloneElement,\n isValidElement,\n type ReactElement,\n type ReactNode,\n useEffect,\n useMemo,\n useRef,\n} from 'react';\nimport { useUserz } from './provider';\n\nexport interface UserzTargetProps {\n /** Stable display name for this target. Surfaces in feedback as the\n * \"component the user clicked on\" label. */\n name: string;\n /** Opt-in metadata you want forwarded with feedback. Keep small + non-sensitive. */\n meta?: Record<string, unknown>;\n /** A single child element (no fragments). We attach a ref to it without\n * a wrapper div so layout is unchanged. */\n children: ReactNode;\n}\n\n/**\n * Wraps a child element to mark it as a \"feedback target\" the end-user can\n * click on while the targeting overlay is active (Ctrl+Shift+U by default).\n *\n * Implementation: clones the single child, attaches a ref, and registers/\n * unregisters with the Userz instance over the child's lifecycle. We do NOT\n * inject a wrapper div — that would break flex/grid layouts. As a result,\n * the child must accept a `ref`; for most DOM elements and `forwardRef`\n * components this is fine. For function components without forwardRef, we\n * fall back to a `display: contents` wrapper.\n */\nexport function UserzTarget({ name, meta, children }: UserzTargetProps) {\n const u = useUserz();\n const ref = useRef<HTMLElement | null>(null);\n // Stable meta identity so the effect only re-runs when shape actually changes.\n // The dep is intentionally the JSON string, not `meta`, so callers passing a\n // fresh object literal each render don't thrash the registration.\n const metaJson = JSON.stringify(meta ?? null);\n // biome-ignore lint/correctness/useExhaustiveDependencies: see above\n const stableMeta = useMemo(() => meta, [metaJson]);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n return u.registerTarget({ el, name, meta: stableMeta });\n }, [u, name, stableMeta]);\n\n const only = Children.only(children);\n if (isValidElement(only) && (typeof only.type === 'string' || hasForwardedRef(only))) {\n const original = (only.props as { ref?: unknown }).ref;\n return cloneElement(only as ReactElement<{ ref?: unknown }>, {\n ref: composeRefs(ref, original),\n });\n }\n // Function component without forwardRef — use a `display: contents` span\n // so we can attach a ref without affecting layout.\n return (\n <span\n ref={(el) => {\n ref.current = el;\n }}\n style={{ display: 'contents' }}\n >\n {only}\n </span>\n );\n}\n\nfunction hasForwardedRef(el: ReactElement): boolean {\n // Conservative heuristic — React.forwardRef components have a \"$$typeof\"\n // marker on their type. We can't import it portably across React versions,\n // so we check the symbol indirectly.\n const t = el.type as unknown as { $$typeof?: symbol };\n return typeof t === 'object' && typeof t.$$typeof?.toString === 'function';\n}\n\nfunction composeRefs<T>(...refs: unknown[]): (node: T) => void {\n return (node: T) => {\n for (const r of refs) {\n if (typeof r === 'function') (r as (n: T) => void)(node);\n else if (r && typeof r === 'object' && 'current' in r) {\n (r as { current: T }).current = node;\n }\n }\n };\n}\n"],"mappings":";;;;;AAUA,MAAM,WAAW,cAA4B,KAAK;;;;;;;;;;;;AAiBlD,SAAgB,cAAc,EAAE,UAAU,GAAG,UAA8B;CACzE,MAAM,WAAW,OAAqB,KAAK;CAG3C,MAAM,YAAY,OAAO,OAAO;CAChC,MAAM,GAAG,YAAY,YAAY,MAAc,IAAI,GAAG,EAAE;AAQxD,KAAI,SAAS,YAAY,QAAQ,OAAO,aAAa,YACnD,UAAS,UAAU,YAAY,UAAU,QAAQ;AAGnD,iBAAgB;AAKd,MAAI,SAAS,YAAY,QAAQ,OAAO,aAAa,aAAa;AAChE,YAAS,UAAU,YAAY,UAAU,QAAQ;AACjD,aAAU;;AAEZ,eAAa;AACX,YAAS,SAAS,SAAS;AAC3B,YAAS,UAAU;;IAEpB,EAAE,CAAC;CAIN,MAAM,cAAc,OAAO;AAC3B,iBAAgB;AACd,MAAI,gBAAgB,OAAW,UAAS,SAAS,QAAQ,YAAY;IACpE,CAAC,YAAY,CAAC;AAEjB,QAAO,oBAAC,SAAS,UAAV;EAAmB,OAAO,SAAS;EAAU;EAA6B;;AAGnF,SAAgB,WAAkB;CAChC,MAAM,IAAI,WAAW,SAAS;AAC9B,KAAI,CAAC,EAAG,OAAM,IAAI,MAAM,+CAA+C;AACvE,QAAO;;;;;ACpBT,SAAgB,iBAAiB,OAAyC;AACxE,QACE,oBAAC,UAAD;EAAU,UAAU,oBAAC,oBAAD,EAAsB;YACxC,oBAAC,YAAD,EAAY,GAAI,OAAS;EAChB;;AAIf,SAAS,WAAW,OAA8B;CAChD,MAAM,CAAC,QAAQ,aAAa,SAA0C,KAAK;AAE3E,iBAAgB;EACd,IAAI,YAAY;AAChB,cAAY,CAAC,MACV,QAAQ;AACP,OAAI,CAAC,UAAW,WAAU,OAAO,UAAU;WAEvC;AACJ,OAAI,CAAC,UAAW,WAAU,UAAU;IAEvC;AACD,eAAa;AACX,eAAY;;IAEb,EAAE,CAAC;AAEN,KAAI,WAAW,KAAM,QAAO,oBAAC,oBAAD,EAAsB;AAClD,KAAI,WAAW,UAAW,QAAO,oBAAC,qBAAD,EAAuB;AAExD,QAAO,oBAAC,cAAD;EAAsB;EAAQ,GAAI;EAAS;;AAiBpD,eAAe,aAA2C;AACxD,KAAI;EAKF,MAAM,MAAO,MAAM,OADN,CAAC,OAAO,MAAM,CAAC,KAAK,GAAG;AAEpC,MAAI,CAAC,IAAI,OAAQ,QAAO;AACxB,SAAO;SACD;AACN,SAAO;;;AAQX,SAAS,aAAa,EACpB,QACA,MACA,OACA,QACA,QACA,UACA,OACA,WACA,aAAa,kBACb,kBAAkB,qBAClB,kBAAkB,SAClB,YAAY,UACQ;CACpB,MAAM,CAAC,QAAQ,aAAa,SAAkB,KAAK;CACnD,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM;CAE3C,MAAM,UAAU,YACd,OAAO,OAAgB;AACrB,YAAU,GAAG;EACb,MAAM,MAAM,IAAI,gBAAgB,KAAK;AACrC,MAAI;GACF,MAAM,OAAO,SAAS,SAAS;IAAE,GAAG;IAAO,GAAG;IAAQ,GAAG,MAAM,YAAY,KAAK;GAChF,MAAM,UAAU,OAAO,gBAAgB,UAAU;GACjD,MAAM,UAAU,OAAO,eAAe;GACtC,MAAM,YAAY;AAIlB,aAAU,aAAa,CACrB;IACE,IAAI;IACJ,MAAM;IACN,UAAU;IACV,OAAO;KACL,MAAM;KACN,KAAK;KACL,GAAG,KAAK;KACR,GAAG,KAAK;KACR,UAAU,KAAK,QAAQ;KACvB,YAAY;KACb;IACD,MAAM,EAAE;IACT,CACF,CAAC;AACF,aAAU,YAAY;IACpB,IAAI;IACJ,MAAM;IACN,GAAG;IACH,GAAG;IACH,OAAO;KAAE;KAAS,GAAG,KAAK;KAAG,GAAG,KAAK;KAAG;IACzC,CAAC;UACI;IAIV;EAAC;EAAQ;EAAM;EAAO;EAAO,CAC9B;CAED,MAAM,aAAa,YAAY,YAAY;AACzC,MAAI,CAAC,UAAU,OAAQ;AACvB,YAAU,KAAK;AACf,MAAI;GACF,MAAM,YAAY;GAClB,MAAM,MAAM,MAAM,KAAK,UAAU,wBAAwB,CAAC;AAE1D,SAAM,OADM,MAAM,OAAO,SAAS,QAAQ,KAAK,OAAO,EAAE,YAAY,OAAO,CAAC,CAC3D;YACT;AACR,aAAU,MAAM;;IAEjB;EAAC;EAAQ;EAAQ;EAAQ;EAAO,CAAC;CAEpC,MAAM,YAAY,OAAO;AAKzB,QACE,qBAAC,OAAD;EACa;EACX,OAAO;GACL,UAAU;GACV,OAAO;GACP,QAAQ;GACR,WAAW;GACX,GAAG;GACJ;YARH,CAUE,oBAAC,WAAD,EAAoB,SAAW,GAC/B,qBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,OAAO;IACP,QAAQ;IACR,SAAS;IACT,KAAK;IACL,QAAQ;IACT;aARH,CAUG,YACC,oBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,OAAO,kBAAkB,SAAS,YAAY,iBAAiB,iBAAiB,UAAU;IAC1F,UAAU;cACX;IAEQ,GAEX,oBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,OAAO,kBAAkB,WAAW,YAAY,iBAAiB,iBAAiB,UAAU;IAC5F,UAAU,UAAU,CAAC;cAEpB,SAAS,YAAY;IACf,EACL;KACF;;;AAIV,SAAS,qBAAqB;AAC5B,QACE,oBAAC,OAAD;EAAK,OAAO;YACV,oBAAC,QAAD,YAAM,mBAAsB;EACxB;;AAIV,SAAS,sBAAsB;AAC7B,QACE,oBAAC,OAAD;EAAK,OAAO;YACV,qBAAC,OAAD;GAAK,OAAO;IAAE,UAAU;IAAK,WAAW;IAAU,YAAY;IAAM;aAApE,CACE,oBAAC,UAAD,YAAQ,sCAA2C,GACnD,qBAAC,OAAD;IAAK,OAAO;KAAE,WAAW;KAAG,UAAU;KAAI,SAAS;KAAK;cAAxD;KAA0D;KAChD,oBAAC,QAAD,YAAM,UAAa;;KAC3B,oBAAC,OAAD;MACE,OAAO;OACL,WAAW;OACX,SAAS;OACT,YAAY;OACZ,cAAc;OACd,UAAU;OACX;gBACF;MAEK;KACF;MACF;;EACF;;AAIV,MAAM,eAA8B;CAClC,SAAS;CACT,YAAY;CACZ,gBAAgB;CAChB,OAAO;CACP,QAAQ;CACR,WAAW;CACX,QAAQ;CACR,cAAc;CACd,UAAU;CACV,OAAO;CACR;AAED,SAAS,kBACP,MACA,YACA,iBACA,iBACA,WACe;AACf,KAAI,SAAS,UACX,QAAO;EACL,SAAS;EACT,cAAc;EACd,QAAQ;EACR,YAAY;EACZ,OAAO;EACP,YAAY;EACZ,QAAQ;EACR,WAAW;EACZ;AAEH,QAAO;EACL,SAAS;EACT,cAAc;EACd,QAAQ;EACR,YAAY;EACZ,OAAO;EACP,QAAQ;EACT;;AAGH,eAAe,YAAY,MAA+C;AACxE,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,MAAM,IAAI,OAAO;EACvB,MAAM,MAAM,IAAI,gBAAgB,KAAK;AACrC,MAAI,eAAe;AACjB,OAAI,gBAAgB,IAAI;AACxB,WAAQ;IAAE,GAAG,IAAI;IAAc,GAAG,IAAI;IAAe,CAAC;;AAExD,MAAI,WAAW,QAAQ;AACrB,OAAI,gBAAgB,IAAI;AACxB,UAAO,IAAI;;AAEb,MAAI,MAAM;GACV;;;;;;;;;;;;;;;;ACnSJ,SAAgB,YAAY,EAAE,MAAM,MAAM,YAA8B;CACtE,MAAM,IAAI,UAAU;CACpB,MAAM,MAAM,OAA2B,KAAK;CAM5C,MAAM,aAAa,cAAc,MAAM,CAFtB,KAAK,UAAU,QAAQ,KAAK,CAEI,CAAC;AAElD,iBAAgB;EACd,MAAM,KAAK,IAAI;AACf,MAAI,CAAC,GAAI;AACT,SAAO,EAAE,eAAe;GAAE;GAAI;GAAM,MAAM;GAAY,CAAC;IACtD;EAAC;EAAG;EAAM;EAAW,CAAC;CAEzB,MAAM,OAAO,SAAS,KAAK,SAAS;AACpC,KAAI,eAAe,KAAK,KAAK,OAAO,KAAK,SAAS,YAAY,gBAAgB,KAAK,GAAG;EACpF,MAAM,WAAY,KAAK,MAA4B;AACnD,SAAO,aAAa,MAAyC,EAC3D,KAAK,YAAY,KAAK,SAAS,EAChC,CAAC;;AAIJ,QACE,oBAAC,QAAD;EACE,MAAM,OAAO;AACX,OAAI,UAAU;;EAEhB,OAAO,EAAE,SAAS,YAAY;YAE7B;EACI;;AAIX,SAAS,gBAAgB,IAA2B;CAIlD,MAAM,IAAI,GAAG;AACb,QAAO,OAAO,MAAM,YAAY,OAAO,EAAE,UAAU,aAAa;;AAGlE,SAAS,YAAe,GAAG,MAAoC;AAC7D,SAAQ,SAAY;AAClB,OAAK,MAAM,KAAK,KACd,KAAI,OAAO,MAAM,WAAY,CAAC,EAAqB,KAAK;WAC/C,KAAK,OAAO,MAAM,YAAY,aAAa,EAClD,CAAC,EAAqB,UAAU"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@userz-ai/react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "React bindings for the Userz feedback widget.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"react": "^18.0.0 || ^19.0.0",
|
|
25
25
|
"tldraw": "^4.0.0",
|
|
26
|
-
"@userz-ai/browser": "0.
|
|
26
|
+
"@userz-ai/browser": "0.3.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependenciesMeta": {
|
|
29
29
|
"tldraw": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"tldraw": "^4.5.9",
|
|
38
38
|
"tsdown": "^0.21.9",
|
|
39
39
|
"typescript": "^6.0.3",
|
|
40
|
-
"@userz-ai/browser": "0.
|
|
40
|
+
"@userz-ai/browser": "0.3.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsdown",
|