@cornercue/react 0.1.0-beta.10

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/dist/index.cjs ADDED
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/FeedbackButton.tsx
7
+
8
+ // src/cdn-loader.ts
9
+ var WIDGET_HASH = "a787f439" ;
10
+ var CDN_BASE = "https://cdn.cornercue.com";
11
+ var coreModuleCache = null;
12
+ var loadPromise = null;
13
+ async function loadCoreCDN() {
14
+ if (coreModuleCache) {
15
+ return coreModuleCache;
16
+ }
17
+ if (loadPromise) {
18
+ return loadPromise;
19
+ }
20
+ loadPromise = (async () => {
21
+ const url = `${CDN_BASE}/widget/core/core.${WIDGET_HASH}.js`;
22
+ try {
23
+ const module = await import(
24
+ /* @vite-ignore */
25
+ url
26
+ );
27
+ coreModuleCache = module;
28
+ return module;
29
+ } catch (error) {
30
+ console.warn(
31
+ `CornerCue: Failed to load core from "${url}", trying latest`,
32
+ error
33
+ );
34
+ const fallbackUrl = `${CDN_BASE}/widget/core/core.latest.js`;
35
+ const module = await import(
36
+ /* @vite-ignore */
37
+ fallbackUrl
38
+ );
39
+ coreModuleCache = module;
40
+ return module;
41
+ }
42
+ })();
43
+ return loadPromise;
44
+ }
45
+ var sharedWidget = null;
46
+ var widgetRefCount = 0;
47
+ async function getSharedWidgetAsync(config) {
48
+ if (!sharedWidget) {
49
+ const { CornerCueWidget } = await loadCoreCDN();
50
+ sharedWidget = new CornerCueWidget(config);
51
+ }
52
+ widgetRefCount++;
53
+ return sharedWidget;
54
+ }
55
+ function releaseSharedWidget() {
56
+ widgetRefCount--;
57
+ if (widgetRefCount <= 0 && sharedWidget) {
58
+ sharedWidget.destroy();
59
+ sharedWidget = null;
60
+ widgetRefCount = 0;
61
+ }
62
+ }
63
+ function prefetchCore() {
64
+ if (typeof window === "undefined") return;
65
+ if ("requestIdleCallback" in window) {
66
+ window.requestIdleCallback(() => {
67
+ loadCoreCDN().catch(() => {
68
+ });
69
+ });
70
+ } else {
71
+ setTimeout(() => {
72
+ loadCoreCDN().catch(() => {
73
+ });
74
+ }, 1e3);
75
+ }
76
+ }
77
+ function FeedbackButton({
78
+ projectId,
79
+ user,
80
+ metadata,
81
+ defaultType,
82
+ config,
83
+ // events - reserved for future use
84
+ children,
85
+ className
86
+ }) {
87
+ const triggerRef = react.useRef(null);
88
+ const widgetRef = react.useRef(null);
89
+ const [isLoading, setIsLoading] = react.useState(false);
90
+ react.useEffect(() => {
91
+ prefetchCore();
92
+ return () => {
93
+ if (widgetRef.current) {
94
+ releaseSharedWidget();
95
+ widgetRef.current = null;
96
+ }
97
+ };
98
+ }, []);
99
+ const handleClick = react.useCallback(
100
+ async (e) => {
101
+ e.preventDefault();
102
+ e.stopPropagation();
103
+ const trigger = triggerRef.current;
104
+ if (!trigger) return;
105
+ if (!widgetRef.current) {
106
+ if (isLoading) return;
107
+ setIsLoading(true);
108
+ try {
109
+ widgetRef.current = await getSharedWidgetAsync(config);
110
+ } catch (error) {
111
+ console.error("CornerCue: Failed to load widget", error);
112
+ setIsLoading(false);
113
+ return;
114
+ }
115
+ setIsLoading(false);
116
+ }
117
+ const widget = widgetRef.current;
118
+ if (widget.isWidgetOpen()) {
119
+ widget.close();
120
+ } else {
121
+ widget.open(trigger, {
122
+ projectId,
123
+ user: user ?? null,
124
+ metadata: metadata ?? null,
125
+ defaultType
126
+ });
127
+ }
128
+ },
129
+ [projectId, user, metadata, defaultType, config, isLoading]
130
+ );
131
+ if (!react.isValidElement(children)) {
132
+ console.warn("FeedbackButton: children must be a valid React element");
133
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
134
+ }
135
+ const childElement = children;
136
+ return react.cloneElement(childElement, {
137
+ ref: triggerRef,
138
+ onClick: (e) => {
139
+ childElement.props.onClick?.(e);
140
+ handleClick(e);
141
+ },
142
+ className: className ? `${childElement.props.className || ""} ${className}`.trim() : childElement.props.className
143
+ });
144
+ }
145
+
146
+ exports.FeedbackButton = FeedbackButton;
147
+ exports.loadCoreCDN = loadCoreCDN;
148
+ exports.prefetchCore = prefetchCore;
149
+ //# sourceMappingURL=index.cjs.map
150
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cdn-loader.ts","../src/FeedbackButton.tsx"],"names":["useRef","useState","useEffect","useCallback","isValidElement","cloneElement"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * CDN Loader for CornerCue Widget Core\n *\n * This module dynamically loads the widget core from CDN, enabling\n * instant updates without requiring npm package updates.\n *\n * Cache strategy:\n * - react-widget.[hash].js: max-age=31536000, immutable (long-cached)\n * - Uses content hash for cache busting\n */\n\nimport type {\n CornerCueWidget as CornerCueWidgetType,\n WidgetConfig,\n} from \"@cornercue/core\";\n\n// Build-time injected hash - replaced by post-build script\ndeclare const __WIDGET_HASH__: string;\nconst WIDGET_HASH =\n typeof __WIDGET_HASH__ !== \"undefined\" ? __WIDGET_HASH__ : \"latest\";\n\n// Build-time flag for dev mode\ndeclare const __DEV__: boolean;\nconst IS_DEV = typeof __DEV__ !== \"undefined\" && __DEV__;\n\n// CDN base URL\nconst CDN_BASE = IS_DEV ? \"\" : \"https://cdn.cornercue.com\";\n\n// Cache for loaded module\nlet coreModuleCache: typeof import(\"@cornercue/core\") | null = null;\nlet loadPromise: Promise<typeof import(\"@cornercue/core\")> | null = null;\n\n/**\n * Load the core widget module from CDN\n */\nexport async function loadCoreCDN(): Promise<typeof import(\"@cornercue/core\")> {\n if (coreModuleCache) {\n return coreModuleCache;\n }\n\n if (loadPromise) {\n return loadPromise;\n }\n\n loadPromise = (async () => {\n const url = IS_DEV\n ? `/widget/packages/core/dist/index.js`\n : `${CDN_BASE}/widget/core/core.${WIDGET_HASH}.js`;\n\n try {\n const module = await import(/* @vite-ignore */ url);\n coreModuleCache = module;\n return module;\n } catch (error) {\n console.warn(\n `CornerCue: Failed to load core from \"${url}\", trying latest`,\n error,\n );\n // Fallback to latest\n const fallbackUrl = IS_DEV\n ? `/widget/packages/core/dist/index.js`\n : `${CDN_BASE}/widget/core/core.latest.js`;\n\n const module = await import(/* @vite-ignore */ fallbackUrl);\n coreModuleCache = module;\n return module;\n }\n })();\n\n return loadPromise;\n}\n\n// Shared widget instance (lazy loaded)\nlet sharedWidget: CornerCueWidgetType | null = null;\nlet widgetRefCount = 0;\n\n/**\n * Get or create a shared widget instance (lazy loaded from CDN)\n */\nexport async function getSharedWidgetAsync(\n config?: WidgetConfig,\n): Promise<CornerCueWidgetType> {\n if (!sharedWidget) {\n const { CornerCueWidget } = await loadCoreCDN();\n sharedWidget = new CornerCueWidget(config);\n }\n widgetRefCount++;\n return sharedWidget;\n}\n\n/**\n * Release reference to shared widget\n */\nexport function releaseSharedWidget(): void {\n widgetRefCount--;\n if (widgetRefCount <= 0 && sharedWidget) {\n sharedWidget.destroy();\n sharedWidget = null;\n widgetRefCount = 0;\n }\n}\n\n/**\n * Prefetch the core module during idle time\n */\nexport function prefetchCore(): void {\n if (typeof window === \"undefined\") return;\n\n if (\"requestIdleCallback\" in window) {\n window.requestIdleCallback(() => {\n loadCoreCDN().catch(() => {\n // Silently fail - will load on demand\n });\n });\n } else {\n // Fallback for Safari\n setTimeout(() => {\n loadCoreCDN().catch(() => {});\n }, 1000);\n }\n}\n","import React, {\n useRef,\n useEffect,\n useCallback,\n useState,\n cloneElement,\n isValidElement,\n type ReactElement,\n type ReactNode,\n type MouseEvent,\n} from \"react\";\nimport type {\n CornerCueWidget,\n WidgetConfig,\n UserInfo,\n Metadata,\n WidgetEvents,\n FeedbackType,\n} from \"@cornercue/core\";\nimport {\n getSharedWidgetAsync,\n releaseSharedWidget,\n prefetchCore,\n} from \"./cdn-loader\";\n\nexport interface FeedbackButtonProps {\n /**\n * Project ID for submissions\n */\n projectId: string;\n\n /**\n * User information to attach to feedback\n */\n user?: UserInfo | null;\n\n /**\n * Additional metadata to attach to feedback\n */\n metadata?: Metadata | null;\n\n /**\n * Default feedback type to open directly (skips menu)\n * Options: 'bug' | 'feature' | 'feedback'\n */\n defaultType?: FeedbackType;\n\n /**\n * Widget configuration overrides\n */\n config?: WidgetConfig;\n\n /**\n * Event callbacks\n */\n events?: WidgetEvents;\n\n /**\n * The trigger element (button, link, etc.)\n */\n children: ReactNode;\n\n /**\n * Additional class name for the wrapper\n */\n className?: string;\n}\n\n/**\n * React component that wraps a trigger element and opens the feedback widget on click.\n * Widget core is lazily loaded from CDN on first interaction.\n *\n * @example\n * ```tsx\n * <FeedbackButton\n * projectId=\"your-project-id\"\n * user={{ id: 'user123', email: 'user@example.com' }}\n * metadata={{ plan: 'pro' }}\n * >\n * <button>Give Feedback</button>\n * </FeedbackButton>\n * ```\n */\nexport function FeedbackButton({\n projectId,\n user,\n metadata,\n defaultType,\n config,\n // events - reserved for future use\n children,\n className,\n}: FeedbackButtonProps): ReactElement {\n const triggerRef = useRef<HTMLElement>(null);\n const widgetRef = useRef<CornerCueWidget | null>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n // Prefetch widget core on mount\n useEffect(() => {\n prefetchCore();\n\n return () => {\n if (widgetRef.current) {\n releaseSharedWidget();\n widgetRef.current = null;\n }\n };\n }, []);\n\n // Handle click on trigger\n const handleClick = useCallback(\n async (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const trigger = triggerRef.current;\n if (!trigger) return;\n\n // Load widget if not already loaded\n if (!widgetRef.current) {\n if (isLoading) return; // Prevent double loading\n setIsLoading(true);\n try {\n widgetRef.current = await getSharedWidgetAsync(config);\n } catch (error) {\n console.error(\"CornerCue: Failed to load widget\", error);\n setIsLoading(false);\n return;\n }\n setIsLoading(false);\n }\n\n const widget = widgetRef.current;\n\n if (widget.isWidgetOpen()) {\n widget.close();\n } else {\n widget.open(trigger, {\n projectId,\n user: user ?? null,\n metadata: metadata ?? null,\n defaultType,\n });\n }\n },\n [projectId, user, metadata, defaultType, config, isLoading],\n );\n\n // Clone the child element to add ref and click handler\n if (!isValidElement(children)) {\n console.warn(\"FeedbackButton: children must be a valid React element\");\n return <>{children}</>;\n }\n\n const childElement = children as ReactElement<{\n ref?: React.Ref<HTMLElement>;\n onClick?: (e: MouseEvent) => void;\n className?: string;\n }>;\n\n return cloneElement(childElement, {\n ref: triggerRef,\n onClick: (e: MouseEvent) => {\n // Call original onClick if present\n childElement.props.onClick?.(e);\n handleClick(e);\n },\n className: className\n ? `${childElement.props.className || \"\"} ${className}`.trim()\n : childElement.props.className,\n });\n}\n\n// Re-export types\nexport type {\n WidgetConfig,\n UserInfo,\n Metadata,\n FeedbackType,\n WidgetEvents,\n OpenOptions,\n} from \"@cornercue/core\";\n"]}
@@ -0,0 +1,67 @@
1
+ import { ReactNode, ReactElement } from 'react';
2
+ import * as _cornercue_core from '@cornercue/core';
3
+ import { UserInfo, Metadata, FeedbackType, WidgetConfig, WidgetEvents } from '@cornercue/core';
4
+ export { FeedbackType, Metadata, OpenOptions, UserInfo, WidgetConfig, WidgetEvents } from '@cornercue/core';
5
+
6
+ interface FeedbackButtonProps {
7
+ /**
8
+ * Project ID for submissions
9
+ */
10
+ projectId: string;
11
+ /**
12
+ * User information to attach to feedback
13
+ */
14
+ user?: UserInfo | null;
15
+ /**
16
+ * Additional metadata to attach to feedback
17
+ */
18
+ metadata?: Metadata | null;
19
+ /**
20
+ * Default feedback type to open directly (skips menu)
21
+ * Options: 'bug' | 'feature' | 'feedback'
22
+ */
23
+ defaultType?: FeedbackType;
24
+ /**
25
+ * Widget configuration overrides
26
+ */
27
+ config?: WidgetConfig;
28
+ /**
29
+ * Event callbacks
30
+ */
31
+ events?: WidgetEvents;
32
+ /**
33
+ * The trigger element (button, link, etc.)
34
+ */
35
+ children: ReactNode;
36
+ /**
37
+ * Additional class name for the wrapper
38
+ */
39
+ className?: string;
40
+ }
41
+ /**
42
+ * React component that wraps a trigger element and opens the feedback widget on click.
43
+ * Widget core is lazily loaded from CDN on first interaction.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * <FeedbackButton
48
+ * projectId="your-project-id"
49
+ * user={{ id: 'user123', email: 'user@example.com' }}
50
+ * metadata={{ plan: 'pro' }}
51
+ * >
52
+ * <button>Give Feedback</button>
53
+ * </FeedbackButton>
54
+ * ```
55
+ */
56
+ declare function FeedbackButton({ projectId, user, metadata, defaultType, config, children, className, }: FeedbackButtonProps): ReactElement;
57
+
58
+ /**
59
+ * Load the core widget module from CDN
60
+ */
61
+ declare function loadCoreCDN(): Promise<typeof _cornercue_core>;
62
+ /**
63
+ * Prefetch the core module during idle time
64
+ */
65
+ declare function prefetchCore(): void;
66
+
67
+ export { FeedbackButton, type FeedbackButtonProps, loadCoreCDN, prefetchCore };
@@ -0,0 +1,67 @@
1
+ import { ReactNode, ReactElement } from 'react';
2
+ import * as _cornercue_core from '@cornercue/core';
3
+ import { UserInfo, Metadata, FeedbackType, WidgetConfig, WidgetEvents } from '@cornercue/core';
4
+ export { FeedbackType, Metadata, OpenOptions, UserInfo, WidgetConfig, WidgetEvents } from '@cornercue/core';
5
+
6
+ interface FeedbackButtonProps {
7
+ /**
8
+ * Project ID for submissions
9
+ */
10
+ projectId: string;
11
+ /**
12
+ * User information to attach to feedback
13
+ */
14
+ user?: UserInfo | null;
15
+ /**
16
+ * Additional metadata to attach to feedback
17
+ */
18
+ metadata?: Metadata | null;
19
+ /**
20
+ * Default feedback type to open directly (skips menu)
21
+ * Options: 'bug' | 'feature' | 'feedback'
22
+ */
23
+ defaultType?: FeedbackType;
24
+ /**
25
+ * Widget configuration overrides
26
+ */
27
+ config?: WidgetConfig;
28
+ /**
29
+ * Event callbacks
30
+ */
31
+ events?: WidgetEvents;
32
+ /**
33
+ * The trigger element (button, link, etc.)
34
+ */
35
+ children: ReactNode;
36
+ /**
37
+ * Additional class name for the wrapper
38
+ */
39
+ className?: string;
40
+ }
41
+ /**
42
+ * React component that wraps a trigger element and opens the feedback widget on click.
43
+ * Widget core is lazily loaded from CDN on first interaction.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * <FeedbackButton
48
+ * projectId="your-project-id"
49
+ * user={{ id: 'user123', email: 'user@example.com' }}
50
+ * metadata={{ plan: 'pro' }}
51
+ * >
52
+ * <button>Give Feedback</button>
53
+ * </FeedbackButton>
54
+ * ```
55
+ */
56
+ declare function FeedbackButton({ projectId, user, metadata, defaultType, config, children, className, }: FeedbackButtonProps): ReactElement;
57
+
58
+ /**
59
+ * Load the core widget module from CDN
60
+ */
61
+ declare function loadCoreCDN(): Promise<typeof _cornercue_core>;
62
+ /**
63
+ * Prefetch the core module during idle time
64
+ */
65
+ declare function prefetchCore(): void;
66
+
67
+ export { FeedbackButton, type FeedbackButtonProps, loadCoreCDN, prefetchCore };
package/dist/index.js ADDED
@@ -0,0 +1,146 @@
1
+ import { useRef, useState, useEffect, useCallback, isValidElement, cloneElement } from 'react';
2
+ import { jsx, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/FeedbackButton.tsx
5
+
6
+ // src/cdn-loader.ts
7
+ var WIDGET_HASH = "a787f439" ;
8
+ var CDN_BASE = "https://cdn.cornercue.com";
9
+ var coreModuleCache = null;
10
+ var loadPromise = null;
11
+ async function loadCoreCDN() {
12
+ if (coreModuleCache) {
13
+ return coreModuleCache;
14
+ }
15
+ if (loadPromise) {
16
+ return loadPromise;
17
+ }
18
+ loadPromise = (async () => {
19
+ const url = `${CDN_BASE}/widget/core/core.${WIDGET_HASH}.js`;
20
+ try {
21
+ const module = await import(
22
+ /* @vite-ignore */
23
+ url
24
+ );
25
+ coreModuleCache = module;
26
+ return module;
27
+ } catch (error) {
28
+ console.warn(
29
+ `CornerCue: Failed to load core from "${url}", trying latest`,
30
+ error
31
+ );
32
+ const fallbackUrl = `${CDN_BASE}/widget/core/core.latest.js`;
33
+ const module = await import(
34
+ /* @vite-ignore */
35
+ fallbackUrl
36
+ );
37
+ coreModuleCache = module;
38
+ return module;
39
+ }
40
+ })();
41
+ return loadPromise;
42
+ }
43
+ var sharedWidget = null;
44
+ var widgetRefCount = 0;
45
+ async function getSharedWidgetAsync(config) {
46
+ if (!sharedWidget) {
47
+ const { CornerCueWidget } = await loadCoreCDN();
48
+ sharedWidget = new CornerCueWidget(config);
49
+ }
50
+ widgetRefCount++;
51
+ return sharedWidget;
52
+ }
53
+ function releaseSharedWidget() {
54
+ widgetRefCount--;
55
+ if (widgetRefCount <= 0 && sharedWidget) {
56
+ sharedWidget.destroy();
57
+ sharedWidget = null;
58
+ widgetRefCount = 0;
59
+ }
60
+ }
61
+ function prefetchCore() {
62
+ if (typeof window === "undefined") return;
63
+ if ("requestIdleCallback" in window) {
64
+ window.requestIdleCallback(() => {
65
+ loadCoreCDN().catch(() => {
66
+ });
67
+ });
68
+ } else {
69
+ setTimeout(() => {
70
+ loadCoreCDN().catch(() => {
71
+ });
72
+ }, 1e3);
73
+ }
74
+ }
75
+ function FeedbackButton({
76
+ projectId,
77
+ user,
78
+ metadata,
79
+ defaultType,
80
+ config,
81
+ // events - reserved for future use
82
+ children,
83
+ className
84
+ }) {
85
+ const triggerRef = useRef(null);
86
+ const widgetRef = useRef(null);
87
+ const [isLoading, setIsLoading] = useState(false);
88
+ useEffect(() => {
89
+ prefetchCore();
90
+ return () => {
91
+ if (widgetRef.current) {
92
+ releaseSharedWidget();
93
+ widgetRef.current = null;
94
+ }
95
+ };
96
+ }, []);
97
+ const handleClick = useCallback(
98
+ async (e) => {
99
+ e.preventDefault();
100
+ e.stopPropagation();
101
+ const trigger = triggerRef.current;
102
+ if (!trigger) return;
103
+ if (!widgetRef.current) {
104
+ if (isLoading) return;
105
+ setIsLoading(true);
106
+ try {
107
+ widgetRef.current = await getSharedWidgetAsync(config);
108
+ } catch (error) {
109
+ console.error("CornerCue: Failed to load widget", error);
110
+ setIsLoading(false);
111
+ return;
112
+ }
113
+ setIsLoading(false);
114
+ }
115
+ const widget = widgetRef.current;
116
+ if (widget.isWidgetOpen()) {
117
+ widget.close();
118
+ } else {
119
+ widget.open(trigger, {
120
+ projectId,
121
+ user: user ?? null,
122
+ metadata: metadata ?? null,
123
+ defaultType
124
+ });
125
+ }
126
+ },
127
+ [projectId, user, metadata, defaultType, config, isLoading]
128
+ );
129
+ if (!isValidElement(children)) {
130
+ console.warn("FeedbackButton: children must be a valid React element");
131
+ return /* @__PURE__ */ jsx(Fragment, { children });
132
+ }
133
+ const childElement = children;
134
+ return cloneElement(childElement, {
135
+ ref: triggerRef,
136
+ onClick: (e) => {
137
+ childElement.props.onClick?.(e);
138
+ handleClick(e);
139
+ },
140
+ className: className ? `${childElement.props.className || ""} ${className}`.trim() : childElement.props.className
141
+ });
142
+ }
143
+
144
+ export { FeedbackButton, loadCoreCDN, prefetchCore };
145
+ //# sourceMappingURL=index.js.map
146
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cdn-loader.ts","../src/FeedbackButton.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * CDN Loader for CornerCue Widget Core\n *\n * This module dynamically loads the widget core from CDN, enabling\n * instant updates without requiring npm package updates.\n *\n * Cache strategy:\n * - react-widget.[hash].js: max-age=31536000, immutable (long-cached)\n * - Uses content hash for cache busting\n */\n\nimport type {\n CornerCueWidget as CornerCueWidgetType,\n WidgetConfig,\n} from \"@cornercue/core\";\n\n// Build-time injected hash - replaced by post-build script\ndeclare const __WIDGET_HASH__: string;\nconst WIDGET_HASH =\n typeof __WIDGET_HASH__ !== \"undefined\" ? __WIDGET_HASH__ : \"latest\";\n\n// Build-time flag for dev mode\ndeclare const __DEV__: boolean;\nconst IS_DEV = typeof __DEV__ !== \"undefined\" && __DEV__;\n\n// CDN base URL\nconst CDN_BASE = IS_DEV ? \"\" : \"https://cdn.cornercue.com\";\n\n// Cache for loaded module\nlet coreModuleCache: typeof import(\"@cornercue/core\") | null = null;\nlet loadPromise: Promise<typeof import(\"@cornercue/core\")> | null = null;\n\n/**\n * Load the core widget module from CDN\n */\nexport async function loadCoreCDN(): Promise<typeof import(\"@cornercue/core\")> {\n if (coreModuleCache) {\n return coreModuleCache;\n }\n\n if (loadPromise) {\n return loadPromise;\n }\n\n loadPromise = (async () => {\n const url = IS_DEV\n ? `/widget/packages/core/dist/index.js`\n : `${CDN_BASE}/widget/core/core.${WIDGET_HASH}.js`;\n\n try {\n const module = await import(/* @vite-ignore */ url);\n coreModuleCache = module;\n return module;\n } catch (error) {\n console.warn(\n `CornerCue: Failed to load core from \"${url}\", trying latest`,\n error,\n );\n // Fallback to latest\n const fallbackUrl = IS_DEV\n ? `/widget/packages/core/dist/index.js`\n : `${CDN_BASE}/widget/core/core.latest.js`;\n\n const module = await import(/* @vite-ignore */ fallbackUrl);\n coreModuleCache = module;\n return module;\n }\n })();\n\n return loadPromise;\n}\n\n// Shared widget instance (lazy loaded)\nlet sharedWidget: CornerCueWidgetType | null = null;\nlet widgetRefCount = 0;\n\n/**\n * Get or create a shared widget instance (lazy loaded from CDN)\n */\nexport async function getSharedWidgetAsync(\n config?: WidgetConfig,\n): Promise<CornerCueWidgetType> {\n if (!sharedWidget) {\n const { CornerCueWidget } = await loadCoreCDN();\n sharedWidget = new CornerCueWidget(config);\n }\n widgetRefCount++;\n return sharedWidget;\n}\n\n/**\n * Release reference to shared widget\n */\nexport function releaseSharedWidget(): void {\n widgetRefCount--;\n if (widgetRefCount <= 0 && sharedWidget) {\n sharedWidget.destroy();\n sharedWidget = null;\n widgetRefCount = 0;\n }\n}\n\n/**\n * Prefetch the core module during idle time\n */\nexport function prefetchCore(): void {\n if (typeof window === \"undefined\") return;\n\n if (\"requestIdleCallback\" in window) {\n window.requestIdleCallback(() => {\n loadCoreCDN().catch(() => {\n // Silently fail - will load on demand\n });\n });\n } else {\n // Fallback for Safari\n setTimeout(() => {\n loadCoreCDN().catch(() => {});\n }, 1000);\n }\n}\n","import React, {\n useRef,\n useEffect,\n useCallback,\n useState,\n cloneElement,\n isValidElement,\n type ReactElement,\n type ReactNode,\n type MouseEvent,\n} from \"react\";\nimport type {\n CornerCueWidget,\n WidgetConfig,\n UserInfo,\n Metadata,\n WidgetEvents,\n FeedbackType,\n} from \"@cornercue/core\";\nimport {\n getSharedWidgetAsync,\n releaseSharedWidget,\n prefetchCore,\n} from \"./cdn-loader\";\n\nexport interface FeedbackButtonProps {\n /**\n * Project ID for submissions\n */\n projectId: string;\n\n /**\n * User information to attach to feedback\n */\n user?: UserInfo | null;\n\n /**\n * Additional metadata to attach to feedback\n */\n metadata?: Metadata | null;\n\n /**\n * Default feedback type to open directly (skips menu)\n * Options: 'bug' | 'feature' | 'feedback'\n */\n defaultType?: FeedbackType;\n\n /**\n * Widget configuration overrides\n */\n config?: WidgetConfig;\n\n /**\n * Event callbacks\n */\n events?: WidgetEvents;\n\n /**\n * The trigger element (button, link, etc.)\n */\n children: ReactNode;\n\n /**\n * Additional class name for the wrapper\n */\n className?: string;\n}\n\n/**\n * React component that wraps a trigger element and opens the feedback widget on click.\n * Widget core is lazily loaded from CDN on first interaction.\n *\n * @example\n * ```tsx\n * <FeedbackButton\n * projectId=\"your-project-id\"\n * user={{ id: 'user123', email: 'user@example.com' }}\n * metadata={{ plan: 'pro' }}\n * >\n * <button>Give Feedback</button>\n * </FeedbackButton>\n * ```\n */\nexport function FeedbackButton({\n projectId,\n user,\n metadata,\n defaultType,\n config,\n // events - reserved for future use\n children,\n className,\n}: FeedbackButtonProps): ReactElement {\n const triggerRef = useRef<HTMLElement>(null);\n const widgetRef = useRef<CornerCueWidget | null>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n // Prefetch widget core on mount\n useEffect(() => {\n prefetchCore();\n\n return () => {\n if (widgetRef.current) {\n releaseSharedWidget();\n widgetRef.current = null;\n }\n };\n }, []);\n\n // Handle click on trigger\n const handleClick = useCallback(\n async (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const trigger = triggerRef.current;\n if (!trigger) return;\n\n // Load widget if not already loaded\n if (!widgetRef.current) {\n if (isLoading) return; // Prevent double loading\n setIsLoading(true);\n try {\n widgetRef.current = await getSharedWidgetAsync(config);\n } catch (error) {\n console.error(\"CornerCue: Failed to load widget\", error);\n setIsLoading(false);\n return;\n }\n setIsLoading(false);\n }\n\n const widget = widgetRef.current;\n\n if (widget.isWidgetOpen()) {\n widget.close();\n } else {\n widget.open(trigger, {\n projectId,\n user: user ?? null,\n metadata: metadata ?? null,\n defaultType,\n });\n }\n },\n [projectId, user, metadata, defaultType, config, isLoading],\n );\n\n // Clone the child element to add ref and click handler\n if (!isValidElement(children)) {\n console.warn(\"FeedbackButton: children must be a valid React element\");\n return <>{children}</>;\n }\n\n const childElement = children as ReactElement<{\n ref?: React.Ref<HTMLElement>;\n onClick?: (e: MouseEvent) => void;\n className?: string;\n }>;\n\n return cloneElement(childElement, {\n ref: triggerRef,\n onClick: (e: MouseEvent) => {\n // Call original onClick if present\n childElement.props.onClick?.(e);\n handleClick(e);\n },\n className: className\n ? `${childElement.props.className || \"\"} ${className}`.trim()\n : childElement.props.className,\n });\n}\n\n// Re-export types\nexport type {\n WidgetConfig,\n UserInfo,\n Metadata,\n FeedbackType,\n WidgetEvents,\n OpenOptions,\n} from \"@cornercue/core\";\n"]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@cornercue/react",
3
+ "version": "0.1.0-beta.10",
4
+ "description": "CornerCue feedback widget - React component",
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
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "DEV=true tsup --watch",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "@cornercue/core": "0.1.0-beta.7"
31
+ },
32
+ "peerDependencies": {
33
+ "react": ">=17.0.0",
34
+ "react-dom": ">=17.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.0.0",
38
+ "@types/react": "^18.2.0",
39
+ "@types/react-dom": "^18.2.0",
40
+ "react": "^18.2.0",
41
+ "react-dom": "^18.2.0",
42
+ "tsup": "^8.0.0",
43
+ "typescript": "^5.3.0"
44
+ },
45
+ "keywords": [
46
+ "feedback",
47
+ "widget",
48
+ "cornercue",
49
+ "react",
50
+ "component"
51
+ ],
52
+ "license": "MIT"
53
+ }