@feedvalue/react 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/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # @feedvalue/react
2
+
3
+ React SDK for FeedValue feedback widget. Provides Provider, Hooks, and Components for React 18+.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @feedvalue/react
9
+ # or
10
+ pnpm add @feedvalue/react
11
+ # or
12
+ yarn add @feedvalue/react
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Setup with Provider
18
+
19
+ ```tsx
20
+ // app/layout.tsx (Next.js App Router)
21
+ import { FeedValueProvider } from '@feedvalue/react';
22
+
23
+ export default function RootLayout({ children }) {
24
+ return (
25
+ <html>
26
+ <body>
27
+ <FeedValueProvider
28
+ widgetId="your-widget-id"
29
+ onReady={() => console.log('Widget ready')}
30
+ onSubmit={(feedback) => console.log('Submitted:', feedback)}
31
+ >
32
+ {children}
33
+ </FeedValueProvider>
34
+ </body>
35
+ </html>
36
+ );
37
+ }
38
+ ```
39
+
40
+ ### Using the Hook
41
+
42
+ ```tsx
43
+ 'use client';
44
+
45
+ import { useFeedValue } from '@feedvalue/react';
46
+
47
+ export function FeedbackButton() {
48
+ const { open, isReady, isOpen } = useFeedValue();
49
+
50
+ return (
51
+ <button onClick={open} disabled={!isReady}>
52
+ {isOpen ? 'Close' : 'Give Feedback'}
53
+ </button>
54
+ );
55
+ }
56
+ ```
57
+
58
+ ### Using the Widget Component
59
+
60
+ ```tsx
61
+ 'use client';
62
+
63
+ import { FeedValueWidget } from '@feedvalue/react';
64
+
65
+ // Standalone widget (no Provider needed)
66
+ export function FeedbackWidget() {
67
+ return (
68
+ <FeedValueWidget
69
+ widgetId="your-widget-id"
70
+ onSubmit={(feedback) => console.log('Submitted:', feedback)}
71
+ />
72
+ );
73
+ }
74
+ ```
75
+
76
+ ### Programmatic Control
77
+
78
+ ```tsx
79
+ 'use client';
80
+
81
+ import { useFeedValue } from '@feedvalue/react';
82
+
83
+ export function FeedbackForm() {
84
+ const { submit, isSubmitting, error } = useFeedValue();
85
+
86
+ const handleSubmit = async (message: string) => {
87
+ try {
88
+ await submit({ message });
89
+ console.log('Feedback submitted successfully');
90
+ } catch (err) {
91
+ console.error('Failed to submit:', err);
92
+ }
93
+ };
94
+
95
+ return (
96
+ <form onSubmit={(e) => {
97
+ e.preventDefault();
98
+ handleSubmit(e.currentTarget.message.value);
99
+ }}>
100
+ <textarea name="message" placeholder="Your feedback..." />
101
+ <button type="submit" disabled={isSubmitting}>
102
+ {isSubmitting ? 'Submitting...' : 'Submit'}
103
+ </button>
104
+ {error && <p className="error">{error.message}</p>}
105
+ </form>
106
+ );
107
+ }
108
+ ```
109
+
110
+ ### User Identification
111
+
112
+ ```tsx
113
+ 'use client';
114
+
115
+ import { useFeedValue } from '@feedvalue/react';
116
+ import { useEffect } from 'react';
117
+
118
+ export function UserIdentifier({ user }) {
119
+ const { identify, setData, reset } = useFeedValue();
120
+
121
+ useEffect(() => {
122
+ if (user) {
123
+ identify(user.id, {
124
+ name: user.name,
125
+ email: user.email,
126
+ plan: user.plan,
127
+ });
128
+ setData({ company: user.company });
129
+ } else {
130
+ reset();
131
+ }
132
+ }, [user, identify, setData, reset]);
133
+
134
+ return null;
135
+ }
136
+ ```
137
+
138
+ ## API Reference
139
+
140
+ ### `<FeedValueProvider>`
141
+
142
+ Provider component for FeedValue context.
143
+
144
+ | Prop | Type | Required | Description |
145
+ |------|------|----------|-------------|
146
+ | `widgetId` | `string` | Yes | Widget ID from FeedValue dashboard |
147
+ | `apiBaseUrl` | `string` | No | Custom API URL (for self-hosted) |
148
+ | `config` | `Partial<FeedValueConfig>` | No | Configuration overrides |
149
+ | `onReady` | `() => void` | No | Called when widget is ready |
150
+ | `onOpen` | `() => void` | No | Called when modal opens |
151
+ | `onClose` | `() => void` | No | Called when modal closes |
152
+ | `onSubmit` | `(feedback: FeedbackData) => void` | No | Called when feedback is submitted |
153
+ | `onError` | `(error: Error) => void` | No | Called on errors |
154
+
155
+ ### `useFeedValue()`
156
+
157
+ Hook to access FeedValue context. Must be used within `<FeedValueProvider>`.
158
+
159
+ Returns:
160
+
161
+ | Property | Type | Description |
162
+ |----------|------|-------------|
163
+ | `isReady` | `boolean` | Widget is initialized |
164
+ | `isOpen` | `boolean` | Modal is open |
165
+ | `isVisible` | `boolean` | Trigger button is visible |
166
+ | `error` | `Error \| null` | Current error |
167
+ | `isSubmitting` | `boolean` | Submission in progress |
168
+ | `open` | `() => void` | Open the modal |
169
+ | `close` | `() => void` | Close the modal |
170
+ | `toggle` | `() => void` | Toggle modal |
171
+ | `show` | `() => void` | Show trigger |
172
+ | `hide` | `() => void` | Hide trigger |
173
+ | `submit` | `(feedback) => Promise<void>` | Submit feedback |
174
+ | `identify` | `(userId, traits?) => void` | Identify user |
175
+ | `setData` | `(data) => void` | Set user data |
176
+ | `reset` | `() => void` | Reset user data |
177
+
178
+ ### `useFeedValueOptional()`
179
+
180
+ Same as `useFeedValue()` but returns `null` if used outside provider instead of throwing.
181
+
182
+ ### `<FeedValueWidget>`
183
+
184
+ Standalone widget component that doesn't require a provider.
185
+
186
+ | Prop | Type | Required | Description |
187
+ |------|------|----------|-------------|
188
+ | `widgetId` | `string` | Yes | Widget ID from FeedValue dashboard |
189
+ | `apiBaseUrl` | `string` | No | Custom API URL |
190
+ | `config` | `Partial<FeedValueConfig>` | No | Configuration overrides |
191
+ | `onReady` | `() => void` | No | Ready callback |
192
+ | `onSubmit` | `(feedback) => void` | No | Submit callback |
193
+ | `onError` | `(error) => void` | No | Error callback |
194
+
195
+ ## Server-Side Rendering
196
+
197
+ The SDK is fully SSR-compatible. It uses `useSyncExternalStore` for concurrent mode support and returns safe default values during server rendering.
198
+
199
+ ```tsx
200
+ // Works out of the box with Next.js App Router
201
+ 'use client';
202
+
203
+ import { useFeedValue } from '@feedvalue/react';
204
+
205
+ export function FeedbackButton() {
206
+ const { open, isReady } = useFeedValue();
207
+
208
+ // isReady is false during SSR, preventing hydration mismatches
209
+ return (
210
+ <button onClick={open} disabled={!isReady}>
211
+ Feedback
212
+ </button>
213
+ );
214
+ }
215
+ ```
216
+
217
+ ## Requirements
218
+
219
+ - React 18.0.0 or higher
220
+ - React DOM 18.0.0 or higher
221
+
222
+ ## License
223
+
224
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var core = require('@feedvalue/core');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ var FeedValueContext = react.createContext(null);
8
+ var getServerSnapshot = () => ({
9
+ isReady: false,
10
+ isOpen: false,
11
+ isVisible: false,
12
+ error: null,
13
+ isSubmitting: false
14
+ });
15
+ function FeedValueProvider({
16
+ widgetId,
17
+ apiBaseUrl,
18
+ config,
19
+ headless,
20
+ children,
21
+ onReady,
22
+ onOpen,
23
+ onClose,
24
+ onSubmit,
25
+ onError
26
+ }) {
27
+ const instanceRef = react.useRef(null);
28
+ const callbacksRef = react.useRef({ onReady, onOpen, onClose, onSubmit, onError });
29
+ callbacksRef.current = { onReady, onOpen, onClose, onSubmit, onError };
30
+ react.useEffect(() => {
31
+ if (typeof window === "undefined") return;
32
+ const instance = core.FeedValue.init({
33
+ widgetId,
34
+ apiBaseUrl,
35
+ config,
36
+ headless
37
+ });
38
+ instanceRef.current = instance;
39
+ const handleReady = () => callbacksRef.current.onReady?.();
40
+ const handleOpen = () => callbacksRef.current.onOpen?.();
41
+ const handleClose = () => callbacksRef.current.onClose?.();
42
+ const handleSubmit = (feedback) => callbacksRef.current.onSubmit?.(feedback);
43
+ const handleError = (error) => callbacksRef.current.onError?.(error);
44
+ instance.on("ready", handleReady);
45
+ instance.on("open", handleOpen);
46
+ instance.on("close", handleClose);
47
+ instance.on("submit", handleSubmit);
48
+ instance.on("error", handleError);
49
+ return () => {
50
+ instance.off("ready", handleReady);
51
+ instance.off("open", handleOpen);
52
+ instance.off("close", handleClose);
53
+ instance.off("submit", handleSubmit);
54
+ instance.off("error", handleError);
55
+ instance.destroy();
56
+ instanceRef.current = null;
57
+ };
58
+ }, [widgetId, apiBaseUrl, headless]);
59
+ const state = react.useSyncExternalStore(
60
+ // Subscribe function
61
+ (callback) => {
62
+ const instance = instanceRef.current;
63
+ if (!instance) return () => {
64
+ };
65
+ return instance.subscribe(callback);
66
+ },
67
+ // getSnapshot - client
68
+ () => instanceRef.current?.getSnapshot() ?? getServerSnapshot(),
69
+ // getServerSnapshot - SSR
70
+ getServerSnapshot
71
+ );
72
+ const open = react.useCallback(() => instanceRef.current?.open(), []);
73
+ const close = react.useCallback(() => instanceRef.current?.close(), []);
74
+ const toggle = react.useCallback(() => instanceRef.current?.toggle(), []);
75
+ const show = react.useCallback(() => instanceRef.current?.show(), []);
76
+ const hide = react.useCallback(() => instanceRef.current?.hide(), []);
77
+ const submit = react.useCallback(
78
+ (feedback) => instanceRef.current?.submit(feedback) ?? Promise.reject(new Error("Not initialized")),
79
+ []
80
+ );
81
+ const identify = react.useCallback(
82
+ (userId, traits) => instanceRef.current?.identify(userId, traits),
83
+ []
84
+ );
85
+ const setData = react.useCallback(
86
+ (data) => instanceRef.current?.setData(data),
87
+ []
88
+ );
89
+ const reset = react.useCallback(() => instanceRef.current?.reset(), []);
90
+ const value = react.useMemo(
91
+ () => ({
92
+ instance: instanceRef.current,
93
+ isReady: state.isReady,
94
+ isOpen: state.isOpen,
95
+ isVisible: state.isVisible,
96
+ error: state.error,
97
+ isSubmitting: state.isSubmitting,
98
+ isHeadless: headless ?? false,
99
+ open,
100
+ close,
101
+ toggle,
102
+ show,
103
+ hide,
104
+ submit,
105
+ identify,
106
+ setData,
107
+ reset
108
+ }),
109
+ [state, headless, open, close, toggle, show, hide, submit, identify, setData, reset]
110
+ );
111
+ return /* @__PURE__ */ jsxRuntime.jsx(FeedValueContext.Provider, { value, children });
112
+ }
113
+ function useFeedValue() {
114
+ const context = react.useContext(FeedValueContext);
115
+ if (!context) {
116
+ throw new Error(
117
+ 'useFeedValue must be used within a FeedValueProvider. Make sure to wrap your app with <FeedValueProvider widgetId="...">.'
118
+ );
119
+ }
120
+ return context;
121
+ }
122
+ function useFeedValueOptional() {
123
+ return react.useContext(FeedValueContext);
124
+ }
125
+ function FeedValueWidget(props) {
126
+ return /* @__PURE__ */ jsxRuntime.jsx(FeedValueProvider, { ...props, children: null });
127
+ }
128
+
129
+ exports.FeedValueProvider = FeedValueProvider;
130
+ exports.FeedValueWidget = FeedValueWidget;
131
+ exports.useFeedValue = useFeedValue;
132
+ exports.useFeedValueOptional = useFeedValueOptional;
133
+ //# sourceMappingURL=index.cjs.map
134
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider.tsx","../src/components.tsx"],"names":["createContext","useRef","useEffect","FeedValue","useSyncExternalStore","useCallback","useMemo","jsx","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["'use client';\n\n/**\n * @feedvalue/react - Provider\n *\n * FeedValueProvider component for React applications.\n * Uses useSyncExternalStore for React 18+ concurrent mode compatibility.\n */\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useSyncExternalStore,\n useMemo,\n type ReactNode,\n} from 'react';\nimport {\n FeedValue,\n type FeedValueConfig,\n type FeedValueState,\n type FeedbackData,\n type UserTraits,\n} from '@feedvalue/core';\n\n/**\n * Context value provided by FeedValueProvider\n */\nexport interface FeedValueContextValue {\n /** FeedValue instance (for advanced usage) */\n instance: FeedValue | null;\n /** Widget is ready (config loaded) */\n isReady: boolean;\n /** Modal is open */\n isOpen: boolean;\n /** Widget is visible */\n isVisible: boolean;\n /** Current error (if any) */\n error: Error | null;\n /** Submission in progress */\n isSubmitting: boolean;\n /** Running in headless mode (no default UI rendered) */\n isHeadless: boolean;\n /** Open the feedback modal */\n open: () => void;\n /** Close the feedback modal */\n close: () => void;\n /** Toggle the feedback modal */\n toggle: () => void;\n /** Show the trigger button */\n show: () => void;\n /** Hide the trigger button */\n hide: () => void;\n /** Submit feedback programmatically */\n submit: (feedback: Partial<FeedbackData>) => Promise<void>;\n /** Identify user */\n identify: (userId: string, traits?: UserTraits) => void;\n /** Set user data */\n setData: (data: Record<string, string>) => void;\n /** Reset user data */\n reset: () => void;\n}\n\n/**\n * FeedValue context (internal)\n */\nconst FeedValueContext = createContext<FeedValueContextValue | null>(null);\n\n/**\n * Props for FeedValueProvider\n */\nexport interface FeedValueProviderProps {\n /** Widget ID from FeedValue dashboard */\n widgetId: string;\n /** API base URL (for self-hosted) */\n apiBaseUrl?: string;\n /** Configuration overrides */\n config?: Partial<FeedValueConfig>;\n /**\n * Headless mode - disables all DOM rendering.\n * Use this when you want full control over the UI.\n * The SDK will still fetch config and provide all API methods\n * but won't render any trigger button or modal.\n *\n * @default false\n */\n headless?: boolean;\n /** Children */\n children: ReactNode;\n /** Callback when widget is ready */\n onReady?: () => void;\n /** Callback when modal opens */\n onOpen?: () => void;\n /** Callback when modal closes */\n onClose?: () => void;\n /** Callback when feedback is submitted */\n onSubmit?: (feedback: FeedbackData) => void;\n /** Callback when an error occurs */\n onError?: (error: Error) => void;\n}\n\n/**\n * Server snapshot for SSR - always returns initial state\n * This prevents hydration mismatches\n */\nconst getServerSnapshot = (): FeedValueState => ({\n isReady: false,\n isOpen: false,\n isVisible: false,\n error: null,\n isSubmitting: false,\n});\n\n/**\n * FeedValueProvider component\n *\n * Provides FeedValue context to child components.\n *\n * @example\n * ```tsx\n * // app/layout.tsx (Next.js App Router)\n * import { FeedValueProvider } from '@feedvalue/react';\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html>\n * <body>\n * <FeedValueProvider\n * widgetId=\"your-widget-id\"\n * onSubmit={(feedback) => console.log('Submitted:', feedback)}\n * >\n * {children}\n * </FeedValueProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function FeedValueProvider({\n widgetId,\n apiBaseUrl,\n config,\n headless,\n children,\n onReady,\n onOpen,\n onClose,\n onSubmit,\n onError,\n}: FeedValueProviderProps): React.ReactElement {\n const instanceRef = useRef<FeedValue | null>(null);\n const callbacksRef = useRef({ onReady, onOpen, onClose, onSubmit, onError });\n\n // Keep callbacks ref updated\n callbacksRef.current = { onReady, onOpen, onClose, onSubmit, onError };\n\n // Initialize on mount\n useEffect(() => {\n // Only initialize on client side\n if (typeof window === 'undefined') return;\n\n const instance = FeedValue.init({\n widgetId,\n apiBaseUrl,\n config,\n headless,\n });\n instanceRef.current = instance;\n\n // Subscribe to events\n const handleReady = () => callbacksRef.current.onReady?.();\n const handleOpen = () => callbacksRef.current.onOpen?.();\n const handleClose = () => callbacksRef.current.onClose?.();\n const handleSubmit = (feedback: FeedbackData) => callbacksRef.current.onSubmit?.(feedback);\n const handleError = (error: Error) => callbacksRef.current.onError?.(error);\n\n instance.on('ready', handleReady);\n instance.on('open', handleOpen);\n instance.on('close', handleClose);\n instance.on('submit', handleSubmit);\n instance.on('error', handleError);\n\n return () => {\n instance.off('ready', handleReady);\n instance.off('open', handleOpen);\n instance.off('close', handleClose);\n instance.off('submit', handleSubmit);\n instance.off('error', handleError);\n instance.destroy();\n instanceRef.current = null;\n };\n }, [widgetId, apiBaseUrl, headless]); // Re-init if widgetId, apiBaseUrl, or headless changes\n\n // Use useSyncExternalStore for concurrent mode compatibility\n const state = useSyncExternalStore(\n // Subscribe function\n (callback) => {\n const instance = instanceRef.current;\n if (!instance) return () => {};\n return instance.subscribe(callback);\n },\n // getSnapshot - client\n () => instanceRef.current?.getSnapshot() ?? getServerSnapshot(),\n // getServerSnapshot - SSR\n getServerSnapshot\n );\n\n // Stable callback references to prevent recreation on every state change\n const open = useCallback(() => instanceRef.current?.open(), []);\n const close = useCallback(() => instanceRef.current?.close(), []);\n const toggle = useCallback(() => instanceRef.current?.toggle(), []);\n const show = useCallback(() => instanceRef.current?.show(), []);\n const hide = useCallback(() => instanceRef.current?.hide(), []);\n const submit = useCallback(\n (feedback: Partial<FeedbackData>) =>\n instanceRef.current?.submit(feedback) ?? Promise.reject(new Error('Not initialized')),\n []\n );\n const identify = useCallback(\n (userId: string, traits?: UserTraits) => instanceRef.current?.identify(userId, traits),\n []\n );\n const setData = useCallback(\n (data: Record<string, string>) => instanceRef.current?.setData(data),\n []\n );\n const reset = useCallback(() => instanceRef.current?.reset(), []);\n\n // Memoize context value to prevent unnecessary re-renders\n const value = useMemo<FeedValueContextValue>(\n () => ({\n instance: instanceRef.current,\n isReady: state.isReady,\n isOpen: state.isOpen,\n isVisible: state.isVisible,\n error: state.error,\n isSubmitting: state.isSubmitting,\n isHeadless: headless ?? false,\n open,\n close,\n toggle,\n show,\n hide,\n submit,\n identify,\n setData,\n reset,\n }),\n [state, headless, open, close, toggle, show, hide, submit, identify, setData, reset]\n );\n\n return (\n <FeedValueContext.Provider value={value}>\n {children}\n </FeedValueContext.Provider>\n );\n}\n\n/**\n * Hook to access FeedValue context\n *\n * Must be used within a FeedValueProvider.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useFeedValue } from '@feedvalue/react';\n *\n * export function FeedbackButton() {\n * const { open, isReady } = useFeedValue();\n *\n * return (\n * <button onClick={open} disabled={!isReady}>\n * Give Feedback\n * </button>\n * );\n * }\n * ```\n */\nexport function useFeedValue(): FeedValueContextValue {\n const context = useContext(FeedValueContext);\n\n if (!context) {\n throw new Error(\n 'useFeedValue must be used within a FeedValueProvider. ' +\n 'Make sure to wrap your app with <FeedValueProvider widgetId=\"...\">.'\n );\n }\n\n return context;\n}\n\n/**\n * Hook to check if inside FeedValueProvider\n * Returns null if outside provider, context value if inside\n */\nexport function useFeedValueOptional(): FeedValueContextValue | null {\n return useContext(FeedValueContext);\n}\n","'use client';\n\n/**\n * @feedvalue/react - Components\n *\n * Standalone React components for FeedValue.\n */\n\nimport React from 'react';\nimport { FeedValueProvider, type FeedValueProviderProps } from './provider';\n\n/**\n * Props for FeedValueWidget component\n */\nexport interface FeedValueWidgetProps extends Omit<FeedValueProviderProps, 'children'> {}\n\n/**\n * Standalone FeedValue widget component\n *\n * Use this when you don't need to access the FeedValue context elsewhere.\n * The widget renders itself via DOM injection - this component is a container.\n *\n * @example\n * ```tsx\n * // Simple usage - just drop in anywhere\n * import { FeedValueWidget } from '@feedvalue/react';\n *\n * export function App() {\n * return (\n * <div>\n * <h1>My App</h1>\n * <FeedValueWidget\n * widgetId=\"your-widget-id\"\n * onSubmit={(feedback) => console.log('Feedback:', feedback)}\n * />\n * </div>\n * );\n * }\n * ```\n */\nexport function FeedValueWidget(props: FeedValueWidgetProps): React.ReactElement {\n return (\n <FeedValueProvider {...props}>\n {/* Widget renders via DOM injection, no children needed */}\n {null}\n </FeedValueProvider>\n );\n}\n"]}
@@ -0,0 +1,173 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { FeedValue, FeedbackData, UserTraits, FeedValueConfig } from '@feedvalue/core';
3
+ export { FeedValueConfig, FeedValueEvents, FeedValueState, FeedbackData, FeedbackMetadata, UserData, UserTraits } from '@feedvalue/core';
4
+
5
+ /**
6
+ * @feedvalue/react - Provider
7
+ *
8
+ * FeedValueProvider component for React applications.
9
+ * Uses useSyncExternalStore for React 18+ concurrent mode compatibility.
10
+ */
11
+
12
+ /**
13
+ * Context value provided by FeedValueProvider
14
+ */
15
+ interface FeedValueContextValue {
16
+ /** FeedValue instance (for advanced usage) */
17
+ instance: FeedValue | null;
18
+ /** Widget is ready (config loaded) */
19
+ isReady: boolean;
20
+ /** Modal is open */
21
+ isOpen: boolean;
22
+ /** Widget is visible */
23
+ isVisible: boolean;
24
+ /** Current error (if any) */
25
+ error: Error | null;
26
+ /** Submission in progress */
27
+ isSubmitting: boolean;
28
+ /** Running in headless mode (no default UI rendered) */
29
+ isHeadless: boolean;
30
+ /** Open the feedback modal */
31
+ open: () => void;
32
+ /** Close the feedback modal */
33
+ close: () => void;
34
+ /** Toggle the feedback modal */
35
+ toggle: () => void;
36
+ /** Show the trigger button */
37
+ show: () => void;
38
+ /** Hide the trigger button */
39
+ hide: () => void;
40
+ /** Submit feedback programmatically */
41
+ submit: (feedback: Partial<FeedbackData>) => Promise<void>;
42
+ /** Identify user */
43
+ identify: (userId: string, traits?: UserTraits) => void;
44
+ /** Set user data */
45
+ setData: (data: Record<string, string>) => void;
46
+ /** Reset user data */
47
+ reset: () => void;
48
+ }
49
+ /**
50
+ * Props for FeedValueProvider
51
+ */
52
+ interface FeedValueProviderProps {
53
+ /** Widget ID from FeedValue dashboard */
54
+ widgetId: string;
55
+ /** API base URL (for self-hosted) */
56
+ apiBaseUrl?: string;
57
+ /** Configuration overrides */
58
+ config?: Partial<FeedValueConfig>;
59
+ /**
60
+ * Headless mode - disables all DOM rendering.
61
+ * Use this when you want full control over the UI.
62
+ * The SDK will still fetch config and provide all API methods
63
+ * but won't render any trigger button or modal.
64
+ *
65
+ * @default false
66
+ */
67
+ headless?: boolean;
68
+ /** Children */
69
+ children: ReactNode;
70
+ /** Callback when widget is ready */
71
+ onReady?: () => void;
72
+ /** Callback when modal opens */
73
+ onOpen?: () => void;
74
+ /** Callback when modal closes */
75
+ onClose?: () => void;
76
+ /** Callback when feedback is submitted */
77
+ onSubmit?: (feedback: FeedbackData) => void;
78
+ /** Callback when an error occurs */
79
+ onError?: (error: Error) => void;
80
+ }
81
+ /**
82
+ * FeedValueProvider component
83
+ *
84
+ * Provides FeedValue context to child components.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * // app/layout.tsx (Next.js App Router)
89
+ * import { FeedValueProvider } from '@feedvalue/react';
90
+ *
91
+ * export default function RootLayout({ children }) {
92
+ * return (
93
+ * <html>
94
+ * <body>
95
+ * <FeedValueProvider
96
+ * widgetId="your-widget-id"
97
+ * onSubmit={(feedback) => console.log('Submitted:', feedback)}
98
+ * >
99
+ * {children}
100
+ * </FeedValueProvider>
101
+ * </body>
102
+ * </html>
103
+ * );
104
+ * }
105
+ * ```
106
+ */
107
+ declare function FeedValueProvider({ widgetId, apiBaseUrl, config, headless, children, onReady, onOpen, onClose, onSubmit, onError, }: FeedValueProviderProps): React.ReactElement;
108
+ /**
109
+ * Hook to access FeedValue context
110
+ *
111
+ * Must be used within a FeedValueProvider.
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * 'use client';
116
+ * import { useFeedValue } from '@feedvalue/react';
117
+ *
118
+ * export function FeedbackButton() {
119
+ * const { open, isReady } = useFeedValue();
120
+ *
121
+ * return (
122
+ * <button onClick={open} disabled={!isReady}>
123
+ * Give Feedback
124
+ * </button>
125
+ * );
126
+ * }
127
+ * ```
128
+ */
129
+ declare function useFeedValue(): FeedValueContextValue;
130
+ /**
131
+ * Hook to check if inside FeedValueProvider
132
+ * Returns null if outside provider, context value if inside
133
+ */
134
+ declare function useFeedValueOptional(): FeedValueContextValue | null;
135
+
136
+ /**
137
+ * @feedvalue/react - Components
138
+ *
139
+ * Standalone React components for FeedValue.
140
+ */
141
+
142
+ /**
143
+ * Props for FeedValueWidget component
144
+ */
145
+ interface FeedValueWidgetProps extends Omit<FeedValueProviderProps, 'children'> {
146
+ }
147
+ /**
148
+ * Standalone FeedValue widget component
149
+ *
150
+ * Use this when you don't need to access the FeedValue context elsewhere.
151
+ * The widget renders itself via DOM injection - this component is a container.
152
+ *
153
+ * @example
154
+ * ```tsx
155
+ * // Simple usage - just drop in anywhere
156
+ * import { FeedValueWidget } from '@feedvalue/react';
157
+ *
158
+ * export function App() {
159
+ * return (
160
+ * <div>
161
+ * <h1>My App</h1>
162
+ * <FeedValueWidget
163
+ * widgetId="your-widget-id"
164
+ * onSubmit={(feedback) => console.log('Feedback:', feedback)}
165
+ * />
166
+ * </div>
167
+ * );
168
+ * }
169
+ * ```
170
+ */
171
+ declare function FeedValueWidget(props: FeedValueWidgetProps): React.ReactElement;
172
+
173
+ export { type FeedValueContextValue, FeedValueProvider, type FeedValueProviderProps, FeedValueWidget, type FeedValueWidgetProps, useFeedValue, useFeedValueOptional };
@@ -0,0 +1,173 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { FeedValue, FeedbackData, UserTraits, FeedValueConfig } from '@feedvalue/core';
3
+ export { FeedValueConfig, FeedValueEvents, FeedValueState, FeedbackData, FeedbackMetadata, UserData, UserTraits } from '@feedvalue/core';
4
+
5
+ /**
6
+ * @feedvalue/react - Provider
7
+ *
8
+ * FeedValueProvider component for React applications.
9
+ * Uses useSyncExternalStore for React 18+ concurrent mode compatibility.
10
+ */
11
+
12
+ /**
13
+ * Context value provided by FeedValueProvider
14
+ */
15
+ interface FeedValueContextValue {
16
+ /** FeedValue instance (for advanced usage) */
17
+ instance: FeedValue | null;
18
+ /** Widget is ready (config loaded) */
19
+ isReady: boolean;
20
+ /** Modal is open */
21
+ isOpen: boolean;
22
+ /** Widget is visible */
23
+ isVisible: boolean;
24
+ /** Current error (if any) */
25
+ error: Error | null;
26
+ /** Submission in progress */
27
+ isSubmitting: boolean;
28
+ /** Running in headless mode (no default UI rendered) */
29
+ isHeadless: boolean;
30
+ /** Open the feedback modal */
31
+ open: () => void;
32
+ /** Close the feedback modal */
33
+ close: () => void;
34
+ /** Toggle the feedback modal */
35
+ toggle: () => void;
36
+ /** Show the trigger button */
37
+ show: () => void;
38
+ /** Hide the trigger button */
39
+ hide: () => void;
40
+ /** Submit feedback programmatically */
41
+ submit: (feedback: Partial<FeedbackData>) => Promise<void>;
42
+ /** Identify user */
43
+ identify: (userId: string, traits?: UserTraits) => void;
44
+ /** Set user data */
45
+ setData: (data: Record<string, string>) => void;
46
+ /** Reset user data */
47
+ reset: () => void;
48
+ }
49
+ /**
50
+ * Props for FeedValueProvider
51
+ */
52
+ interface FeedValueProviderProps {
53
+ /** Widget ID from FeedValue dashboard */
54
+ widgetId: string;
55
+ /** API base URL (for self-hosted) */
56
+ apiBaseUrl?: string;
57
+ /** Configuration overrides */
58
+ config?: Partial<FeedValueConfig>;
59
+ /**
60
+ * Headless mode - disables all DOM rendering.
61
+ * Use this when you want full control over the UI.
62
+ * The SDK will still fetch config and provide all API methods
63
+ * but won't render any trigger button or modal.
64
+ *
65
+ * @default false
66
+ */
67
+ headless?: boolean;
68
+ /** Children */
69
+ children: ReactNode;
70
+ /** Callback when widget is ready */
71
+ onReady?: () => void;
72
+ /** Callback when modal opens */
73
+ onOpen?: () => void;
74
+ /** Callback when modal closes */
75
+ onClose?: () => void;
76
+ /** Callback when feedback is submitted */
77
+ onSubmit?: (feedback: FeedbackData) => void;
78
+ /** Callback when an error occurs */
79
+ onError?: (error: Error) => void;
80
+ }
81
+ /**
82
+ * FeedValueProvider component
83
+ *
84
+ * Provides FeedValue context to child components.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * // app/layout.tsx (Next.js App Router)
89
+ * import { FeedValueProvider } from '@feedvalue/react';
90
+ *
91
+ * export default function RootLayout({ children }) {
92
+ * return (
93
+ * <html>
94
+ * <body>
95
+ * <FeedValueProvider
96
+ * widgetId="your-widget-id"
97
+ * onSubmit={(feedback) => console.log('Submitted:', feedback)}
98
+ * >
99
+ * {children}
100
+ * </FeedValueProvider>
101
+ * </body>
102
+ * </html>
103
+ * );
104
+ * }
105
+ * ```
106
+ */
107
+ declare function FeedValueProvider({ widgetId, apiBaseUrl, config, headless, children, onReady, onOpen, onClose, onSubmit, onError, }: FeedValueProviderProps): React.ReactElement;
108
+ /**
109
+ * Hook to access FeedValue context
110
+ *
111
+ * Must be used within a FeedValueProvider.
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * 'use client';
116
+ * import { useFeedValue } from '@feedvalue/react';
117
+ *
118
+ * export function FeedbackButton() {
119
+ * const { open, isReady } = useFeedValue();
120
+ *
121
+ * return (
122
+ * <button onClick={open} disabled={!isReady}>
123
+ * Give Feedback
124
+ * </button>
125
+ * );
126
+ * }
127
+ * ```
128
+ */
129
+ declare function useFeedValue(): FeedValueContextValue;
130
+ /**
131
+ * Hook to check if inside FeedValueProvider
132
+ * Returns null if outside provider, context value if inside
133
+ */
134
+ declare function useFeedValueOptional(): FeedValueContextValue | null;
135
+
136
+ /**
137
+ * @feedvalue/react - Components
138
+ *
139
+ * Standalone React components for FeedValue.
140
+ */
141
+
142
+ /**
143
+ * Props for FeedValueWidget component
144
+ */
145
+ interface FeedValueWidgetProps extends Omit<FeedValueProviderProps, 'children'> {
146
+ }
147
+ /**
148
+ * Standalone FeedValue widget component
149
+ *
150
+ * Use this when you don't need to access the FeedValue context elsewhere.
151
+ * The widget renders itself via DOM injection - this component is a container.
152
+ *
153
+ * @example
154
+ * ```tsx
155
+ * // Simple usage - just drop in anywhere
156
+ * import { FeedValueWidget } from '@feedvalue/react';
157
+ *
158
+ * export function App() {
159
+ * return (
160
+ * <div>
161
+ * <h1>My App</h1>
162
+ * <FeedValueWidget
163
+ * widgetId="your-widget-id"
164
+ * onSubmit={(feedback) => console.log('Feedback:', feedback)}
165
+ * />
166
+ * </div>
167
+ * );
168
+ * }
169
+ * ```
170
+ */
171
+ declare function FeedValueWidget(props: FeedValueWidgetProps): React.ReactElement;
172
+
173
+ export { type FeedValueContextValue, FeedValueProvider, type FeedValueProviderProps, FeedValueWidget, type FeedValueWidgetProps, useFeedValue, useFeedValueOptional };
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
1
+ import { createContext, useRef, useEffect, useSyncExternalStore, useCallback, useMemo, useContext } from 'react';
2
+ import { FeedValue } from '@feedvalue/core';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ var FeedValueContext = createContext(null);
6
+ var getServerSnapshot = () => ({
7
+ isReady: false,
8
+ isOpen: false,
9
+ isVisible: false,
10
+ error: null,
11
+ isSubmitting: false
12
+ });
13
+ function FeedValueProvider({
14
+ widgetId,
15
+ apiBaseUrl,
16
+ config,
17
+ headless,
18
+ children,
19
+ onReady,
20
+ onOpen,
21
+ onClose,
22
+ onSubmit,
23
+ onError
24
+ }) {
25
+ const instanceRef = useRef(null);
26
+ const callbacksRef = useRef({ onReady, onOpen, onClose, onSubmit, onError });
27
+ callbacksRef.current = { onReady, onOpen, onClose, onSubmit, onError };
28
+ useEffect(() => {
29
+ if (typeof window === "undefined") return;
30
+ const instance = FeedValue.init({
31
+ widgetId,
32
+ apiBaseUrl,
33
+ config,
34
+ headless
35
+ });
36
+ instanceRef.current = instance;
37
+ const handleReady = () => callbacksRef.current.onReady?.();
38
+ const handleOpen = () => callbacksRef.current.onOpen?.();
39
+ const handleClose = () => callbacksRef.current.onClose?.();
40
+ const handleSubmit = (feedback) => callbacksRef.current.onSubmit?.(feedback);
41
+ const handleError = (error) => callbacksRef.current.onError?.(error);
42
+ instance.on("ready", handleReady);
43
+ instance.on("open", handleOpen);
44
+ instance.on("close", handleClose);
45
+ instance.on("submit", handleSubmit);
46
+ instance.on("error", handleError);
47
+ return () => {
48
+ instance.off("ready", handleReady);
49
+ instance.off("open", handleOpen);
50
+ instance.off("close", handleClose);
51
+ instance.off("submit", handleSubmit);
52
+ instance.off("error", handleError);
53
+ instance.destroy();
54
+ instanceRef.current = null;
55
+ };
56
+ }, [widgetId, apiBaseUrl, headless]);
57
+ const state = useSyncExternalStore(
58
+ // Subscribe function
59
+ (callback) => {
60
+ const instance = instanceRef.current;
61
+ if (!instance) return () => {
62
+ };
63
+ return instance.subscribe(callback);
64
+ },
65
+ // getSnapshot - client
66
+ () => instanceRef.current?.getSnapshot() ?? getServerSnapshot(),
67
+ // getServerSnapshot - SSR
68
+ getServerSnapshot
69
+ );
70
+ const open = useCallback(() => instanceRef.current?.open(), []);
71
+ const close = useCallback(() => instanceRef.current?.close(), []);
72
+ const toggle = useCallback(() => instanceRef.current?.toggle(), []);
73
+ const show = useCallback(() => instanceRef.current?.show(), []);
74
+ const hide = useCallback(() => instanceRef.current?.hide(), []);
75
+ const submit = useCallback(
76
+ (feedback) => instanceRef.current?.submit(feedback) ?? Promise.reject(new Error("Not initialized")),
77
+ []
78
+ );
79
+ const identify = useCallback(
80
+ (userId, traits) => instanceRef.current?.identify(userId, traits),
81
+ []
82
+ );
83
+ const setData = useCallback(
84
+ (data) => instanceRef.current?.setData(data),
85
+ []
86
+ );
87
+ const reset = useCallback(() => instanceRef.current?.reset(), []);
88
+ const value = useMemo(
89
+ () => ({
90
+ instance: instanceRef.current,
91
+ isReady: state.isReady,
92
+ isOpen: state.isOpen,
93
+ isVisible: state.isVisible,
94
+ error: state.error,
95
+ isSubmitting: state.isSubmitting,
96
+ isHeadless: headless ?? false,
97
+ open,
98
+ close,
99
+ toggle,
100
+ show,
101
+ hide,
102
+ submit,
103
+ identify,
104
+ setData,
105
+ reset
106
+ }),
107
+ [state, headless, open, close, toggle, show, hide, submit, identify, setData, reset]
108
+ );
109
+ return /* @__PURE__ */ jsx(FeedValueContext.Provider, { value, children });
110
+ }
111
+ function useFeedValue() {
112
+ const context = useContext(FeedValueContext);
113
+ if (!context) {
114
+ throw new Error(
115
+ 'useFeedValue must be used within a FeedValueProvider. Make sure to wrap your app with <FeedValueProvider widgetId="...">.'
116
+ );
117
+ }
118
+ return context;
119
+ }
120
+ function useFeedValueOptional() {
121
+ return useContext(FeedValueContext);
122
+ }
123
+ function FeedValueWidget(props) {
124
+ return /* @__PURE__ */ jsx(FeedValueProvider, { ...props, children: null });
125
+ }
126
+
127
+ export { FeedValueProvider, FeedValueWidget, useFeedValue, useFeedValueOptional };
128
+ //# sourceMappingURL=index.js.map
129
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider.tsx","../src/components.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["'use client';\n\n/**\n * @feedvalue/react - Provider\n *\n * FeedValueProvider component for React applications.\n * Uses useSyncExternalStore for React 18+ concurrent mode compatibility.\n */\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useSyncExternalStore,\n useMemo,\n type ReactNode,\n} from 'react';\nimport {\n FeedValue,\n type FeedValueConfig,\n type FeedValueState,\n type FeedbackData,\n type UserTraits,\n} from '@feedvalue/core';\n\n/**\n * Context value provided by FeedValueProvider\n */\nexport interface FeedValueContextValue {\n /** FeedValue instance (for advanced usage) */\n instance: FeedValue | null;\n /** Widget is ready (config loaded) */\n isReady: boolean;\n /** Modal is open */\n isOpen: boolean;\n /** Widget is visible */\n isVisible: boolean;\n /** Current error (if any) */\n error: Error | null;\n /** Submission in progress */\n isSubmitting: boolean;\n /** Running in headless mode (no default UI rendered) */\n isHeadless: boolean;\n /** Open the feedback modal */\n open: () => void;\n /** Close the feedback modal */\n close: () => void;\n /** Toggle the feedback modal */\n toggle: () => void;\n /** Show the trigger button */\n show: () => void;\n /** Hide the trigger button */\n hide: () => void;\n /** Submit feedback programmatically */\n submit: (feedback: Partial<FeedbackData>) => Promise<void>;\n /** Identify user */\n identify: (userId: string, traits?: UserTraits) => void;\n /** Set user data */\n setData: (data: Record<string, string>) => void;\n /** Reset user data */\n reset: () => void;\n}\n\n/**\n * FeedValue context (internal)\n */\nconst FeedValueContext = createContext<FeedValueContextValue | null>(null);\n\n/**\n * Props for FeedValueProvider\n */\nexport interface FeedValueProviderProps {\n /** Widget ID from FeedValue dashboard */\n widgetId: string;\n /** API base URL (for self-hosted) */\n apiBaseUrl?: string;\n /** Configuration overrides */\n config?: Partial<FeedValueConfig>;\n /**\n * Headless mode - disables all DOM rendering.\n * Use this when you want full control over the UI.\n * The SDK will still fetch config and provide all API methods\n * but won't render any trigger button or modal.\n *\n * @default false\n */\n headless?: boolean;\n /** Children */\n children: ReactNode;\n /** Callback when widget is ready */\n onReady?: () => void;\n /** Callback when modal opens */\n onOpen?: () => void;\n /** Callback when modal closes */\n onClose?: () => void;\n /** Callback when feedback is submitted */\n onSubmit?: (feedback: FeedbackData) => void;\n /** Callback when an error occurs */\n onError?: (error: Error) => void;\n}\n\n/**\n * Server snapshot for SSR - always returns initial state\n * This prevents hydration mismatches\n */\nconst getServerSnapshot = (): FeedValueState => ({\n isReady: false,\n isOpen: false,\n isVisible: false,\n error: null,\n isSubmitting: false,\n});\n\n/**\n * FeedValueProvider component\n *\n * Provides FeedValue context to child components.\n *\n * @example\n * ```tsx\n * // app/layout.tsx (Next.js App Router)\n * import { FeedValueProvider } from '@feedvalue/react';\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html>\n * <body>\n * <FeedValueProvider\n * widgetId=\"your-widget-id\"\n * onSubmit={(feedback) => console.log('Submitted:', feedback)}\n * >\n * {children}\n * </FeedValueProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function FeedValueProvider({\n widgetId,\n apiBaseUrl,\n config,\n headless,\n children,\n onReady,\n onOpen,\n onClose,\n onSubmit,\n onError,\n}: FeedValueProviderProps): React.ReactElement {\n const instanceRef = useRef<FeedValue | null>(null);\n const callbacksRef = useRef({ onReady, onOpen, onClose, onSubmit, onError });\n\n // Keep callbacks ref updated\n callbacksRef.current = { onReady, onOpen, onClose, onSubmit, onError };\n\n // Initialize on mount\n useEffect(() => {\n // Only initialize on client side\n if (typeof window === 'undefined') return;\n\n const instance = FeedValue.init({\n widgetId,\n apiBaseUrl,\n config,\n headless,\n });\n instanceRef.current = instance;\n\n // Subscribe to events\n const handleReady = () => callbacksRef.current.onReady?.();\n const handleOpen = () => callbacksRef.current.onOpen?.();\n const handleClose = () => callbacksRef.current.onClose?.();\n const handleSubmit = (feedback: FeedbackData) => callbacksRef.current.onSubmit?.(feedback);\n const handleError = (error: Error) => callbacksRef.current.onError?.(error);\n\n instance.on('ready', handleReady);\n instance.on('open', handleOpen);\n instance.on('close', handleClose);\n instance.on('submit', handleSubmit);\n instance.on('error', handleError);\n\n return () => {\n instance.off('ready', handleReady);\n instance.off('open', handleOpen);\n instance.off('close', handleClose);\n instance.off('submit', handleSubmit);\n instance.off('error', handleError);\n instance.destroy();\n instanceRef.current = null;\n };\n }, [widgetId, apiBaseUrl, headless]); // Re-init if widgetId, apiBaseUrl, or headless changes\n\n // Use useSyncExternalStore for concurrent mode compatibility\n const state = useSyncExternalStore(\n // Subscribe function\n (callback) => {\n const instance = instanceRef.current;\n if (!instance) return () => {};\n return instance.subscribe(callback);\n },\n // getSnapshot - client\n () => instanceRef.current?.getSnapshot() ?? getServerSnapshot(),\n // getServerSnapshot - SSR\n getServerSnapshot\n );\n\n // Stable callback references to prevent recreation on every state change\n const open = useCallback(() => instanceRef.current?.open(), []);\n const close = useCallback(() => instanceRef.current?.close(), []);\n const toggle = useCallback(() => instanceRef.current?.toggle(), []);\n const show = useCallback(() => instanceRef.current?.show(), []);\n const hide = useCallback(() => instanceRef.current?.hide(), []);\n const submit = useCallback(\n (feedback: Partial<FeedbackData>) =>\n instanceRef.current?.submit(feedback) ?? Promise.reject(new Error('Not initialized')),\n []\n );\n const identify = useCallback(\n (userId: string, traits?: UserTraits) => instanceRef.current?.identify(userId, traits),\n []\n );\n const setData = useCallback(\n (data: Record<string, string>) => instanceRef.current?.setData(data),\n []\n );\n const reset = useCallback(() => instanceRef.current?.reset(), []);\n\n // Memoize context value to prevent unnecessary re-renders\n const value = useMemo<FeedValueContextValue>(\n () => ({\n instance: instanceRef.current,\n isReady: state.isReady,\n isOpen: state.isOpen,\n isVisible: state.isVisible,\n error: state.error,\n isSubmitting: state.isSubmitting,\n isHeadless: headless ?? false,\n open,\n close,\n toggle,\n show,\n hide,\n submit,\n identify,\n setData,\n reset,\n }),\n [state, headless, open, close, toggle, show, hide, submit, identify, setData, reset]\n );\n\n return (\n <FeedValueContext.Provider value={value}>\n {children}\n </FeedValueContext.Provider>\n );\n}\n\n/**\n * Hook to access FeedValue context\n *\n * Must be used within a FeedValueProvider.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useFeedValue } from '@feedvalue/react';\n *\n * export function FeedbackButton() {\n * const { open, isReady } = useFeedValue();\n *\n * return (\n * <button onClick={open} disabled={!isReady}>\n * Give Feedback\n * </button>\n * );\n * }\n * ```\n */\nexport function useFeedValue(): FeedValueContextValue {\n const context = useContext(FeedValueContext);\n\n if (!context) {\n throw new Error(\n 'useFeedValue must be used within a FeedValueProvider. ' +\n 'Make sure to wrap your app with <FeedValueProvider widgetId=\"...\">.'\n );\n }\n\n return context;\n}\n\n/**\n * Hook to check if inside FeedValueProvider\n * Returns null if outside provider, context value if inside\n */\nexport function useFeedValueOptional(): FeedValueContextValue | null {\n return useContext(FeedValueContext);\n}\n","'use client';\n\n/**\n * @feedvalue/react - Components\n *\n * Standalone React components for FeedValue.\n */\n\nimport React from 'react';\nimport { FeedValueProvider, type FeedValueProviderProps } from './provider';\n\n/**\n * Props for FeedValueWidget component\n */\nexport interface FeedValueWidgetProps extends Omit<FeedValueProviderProps, 'children'> {}\n\n/**\n * Standalone FeedValue widget component\n *\n * Use this when you don't need to access the FeedValue context elsewhere.\n * The widget renders itself via DOM injection - this component is a container.\n *\n * @example\n * ```tsx\n * // Simple usage - just drop in anywhere\n * import { FeedValueWidget } from '@feedvalue/react';\n *\n * export function App() {\n * return (\n * <div>\n * <h1>My App</h1>\n * <FeedValueWidget\n * widgetId=\"your-widget-id\"\n * onSubmit={(feedback) => console.log('Feedback:', feedback)}\n * />\n * </div>\n * );\n * }\n * ```\n */\nexport function FeedValueWidget(props: FeedValueWidgetProps): React.ReactElement {\n return (\n <FeedValueProvider {...props}>\n {/* Widget renders via DOM injection, no children needed */}\n {null}\n </FeedValueProvider>\n );\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@feedvalue/react",
3
+ "version": "0.1.0",
4
+ "description": "FeedValue React SDK - Provider, Hooks, and Components for React 18+",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "sideEffects": false,
26
+ "peerDependencies": {
27
+ "react": ">=18.0.0",
28
+ "react-dom": ">=18.0.0"
29
+ },
30
+ "dependencies": {
31
+ "@feedvalue/core": "^0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "@testing-library/jest-dom": "^6.9.1",
35
+ "@testing-library/react": "^16.1.0",
36
+ "@types/react": "^19.0.0",
37
+ "@types/react-dom": "^19.0.0",
38
+ "@vitest/coverage-v8": "^2.1.0",
39
+ "eslint": "^9.17.0",
40
+ "happy-dom": "^15.11.0",
41
+ "react": "^19.0.0",
42
+ "react-dom": "^19.0.0",
43
+ "tsup": "^8.3.0",
44
+ "typescript": "^5.7.0",
45
+ "vitest": "^2.1.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/sarverenterprises/feedvalue-packages.git",
53
+ "directory": "packages/react"
54
+ },
55
+ "homepage": "https://feedvalue.com",
56
+ "bugs": {
57
+ "url": "https://github.com/sarverenterprises/feedvalue-packages/issues"
58
+ },
59
+ "license": "MIT",
60
+ "author": "Sarver Enterprises",
61
+ "keywords": [
62
+ "feedvalue",
63
+ "feedback",
64
+ "widget",
65
+ "react",
66
+ "hooks",
67
+ "nextjs"
68
+ ],
69
+ "engines": {
70
+ "node": ">=18"
71
+ },
72
+ "scripts": {
73
+ "build": "tsup",
74
+ "dev": "tsup --watch",
75
+ "lint": "eslint src --ext .ts,.tsx",
76
+ "test": "vitest run",
77
+ "test:watch": "vitest",
78
+ "test:coverage": "vitest run --coverage",
79
+ "typecheck": "tsc --noEmit",
80
+ "clean": "rm -rf dist"
81
+ }
82
+ }