react-sharesheet 1.7.0 → 1.9.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.
@@ -17,7 +17,7 @@ declare const CSS_VAR_UI_DEFAULTS: {
17
17
  readonly "--sharesheet-overlay-bg": "rgba(0, 0, 0, 0.7)";
18
18
  readonly "--sharesheet-drawer-bg": "#09090b";
19
19
  readonly "--sharesheet-drawer-border": "#27272a";
20
- readonly "--sharesheet-handle-bg": "#27272a";
20
+ readonly "--sharesheet-handle-bg": "rgba(255, 255, 255, 0.2)";
21
21
  readonly "--sharesheet-title-color": "#ffffff";
22
22
  readonly "--sharesheet-subtitle-color": "#a1a1aa";
23
23
  readonly "--sharesheet-button-label-color": "#ffffff";
@@ -85,6 +85,12 @@ interface ShareSheetContentProps {
85
85
  downloadUrl?: string | null;
86
86
  /** Filename for downloaded file */
87
87
  downloadFilename?: string;
88
+ /** Custom preview image URL (skips OG fetching if provided) */
89
+ previewImage?: string | null;
90
+ /** File to share via native share (data URL or Blob) - takes priority over URL sharing */
91
+ shareFile?: string | Blob | null;
92
+ /** Filename for the shared file */
93
+ shareFilename?: string;
88
94
  /** Custom class name for the container (shorthand for classNames.root) */
89
95
  className?: string;
90
96
  /** Override class names for sub-components */
@@ -99,6 +105,8 @@ interface ShareSheetContentProps {
99
105
  onCopy?: () => void;
100
106
  /** Called when download starts */
101
107
  onDownload?: () => void;
108
+ /** Called on every share option click, with the option id (analytics-friendly) */
109
+ onShare?: (option: ShareOption) => void;
102
110
  /** Hide specific share options */
103
111
  hide?: ShareOption[];
104
112
  /** Show only these platforms, in this exact order */
@@ -199,7 +207,7 @@ declare const CSS_VAR_DEFAULTS: {
199
207
  readonly "--sharesheet-overlay-bg": "rgba(0, 0, 0, 0.7)";
200
208
  readonly "--sharesheet-drawer-bg": "#09090b";
201
209
  readonly "--sharesheet-drawer-border": "#27272a";
202
- readonly "--sharesheet-handle-bg": "#27272a";
210
+ readonly "--sharesheet-handle-bg": "rgba(255, 255, 255, 0.2)";
203
211
  readonly "--sharesheet-title-color": "#ffffff";
204
212
  readonly "--sharesheet-subtitle-color": "#a1a1aa";
205
213
  readonly "--sharesheet-button-label-color": "#ffffff";
@@ -17,7 +17,7 @@ declare const CSS_VAR_UI_DEFAULTS: {
17
17
  readonly "--sharesheet-overlay-bg": "rgba(0, 0, 0, 0.7)";
18
18
  readonly "--sharesheet-drawer-bg": "#09090b";
19
19
  readonly "--sharesheet-drawer-border": "#27272a";
20
- readonly "--sharesheet-handle-bg": "#27272a";
20
+ readonly "--sharesheet-handle-bg": "rgba(255, 255, 255, 0.2)";
21
21
  readonly "--sharesheet-title-color": "#ffffff";
22
22
  readonly "--sharesheet-subtitle-color": "#a1a1aa";
23
23
  readonly "--sharesheet-button-label-color": "#ffffff";
@@ -85,6 +85,12 @@ interface ShareSheetContentProps {
85
85
  downloadUrl?: string | null;
86
86
  /** Filename for downloaded file */
87
87
  downloadFilename?: string;
88
+ /** Custom preview image URL (skips OG fetching if provided) */
89
+ previewImage?: string | null;
90
+ /** File to share via native share (data URL or Blob) - takes priority over URL sharing */
91
+ shareFile?: string | Blob | null;
92
+ /** Filename for the shared file */
93
+ shareFilename?: string;
88
94
  /** Custom class name for the container (shorthand for classNames.root) */
89
95
  className?: string;
90
96
  /** Override class names for sub-components */
@@ -99,6 +105,8 @@ interface ShareSheetContentProps {
99
105
  onCopy?: () => void;
100
106
  /** Called when download starts */
101
107
  onDownload?: () => void;
108
+ /** Called on every share option click, with the option id (analytics-friendly) */
109
+ onShare?: (option: ShareOption) => void;
102
110
  /** Hide specific share options */
103
111
  hide?: ShareOption[];
104
112
  /** Show only these platforms, in this exact order */
@@ -199,7 +207,7 @@ declare const CSS_VAR_DEFAULTS: {
199
207
  readonly "--sharesheet-overlay-bg": "rgba(0, 0, 0, 0.7)";
200
208
  readonly "--sharesheet-drawer-bg": "#09090b";
201
209
  readonly "--sharesheet-drawer-border": "#27272a";
202
- readonly "--sharesheet-handle-bg": "#27272a";
210
+ readonly "--sharesheet-handle-bg": "rgba(255, 255, 255, 0.2)";
203
211
  readonly "--sharesheet-title-color": "#ffffff";
204
212
  readonly "--sharesheet-subtitle-color": "#a1a1aa";
205
213
  readonly "--sharesheet-button-label-color": "#ffffff";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-sharesheet",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "A mobile-first share sheet for React with native share support, Open Graph previews, 15+ social platforms, and headless APIs. Tailwind-styled by default, fully customizable.",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -42,7 +42,7 @@
42
42
  "test:watch": "vitest",
43
43
  "test:coverage": "vitest run --coverage",
44
44
  "prepack": "npm run clean && npm run build",
45
- "prepublishOnly": "npm run clean && npm run build",
45
+ "prepublishOnly": "pnpm run type-check && pnpm run test && pnpm run build",
46
46
  "publish-patch": "npm version patch && npm publish",
47
47
  "publish-minor": "npm version minor && npm publish",
48
48
  "publish-major": "npm version major && npm publish",
@@ -28,9 +28,9 @@ const defaultClasses = {
28
28
  header: "text-center mb-2",
29
29
  title: "text-2xl font-black",
30
30
  subtitle: "mt-1 text-sm",
31
- preview: "flex justify-center mb-4 px-4",
31
+ preview: "flex justify-center my-4 px-4",
32
32
  previewSkeleton: "rounded-xl overflow-hidden",
33
- previewImage: "border border-white/10",
33
+ previewImage: "border border-white/10 rounded-xl",
34
34
  previewMeta: "",
35
35
  grid: "px-2 py-6 flex flex-row items-start gap-4 gap-y-6 flex-wrap justify-center",
36
36
  button: "flex flex-col items-center gap-0 text-xs w-[60px] outline-none cursor-pointer group",
@@ -38,12 +38,19 @@ const defaultClasses = {
38
38
  buttonLabel: "",
39
39
  };
40
40
 
41
- // Shimmer keyframes as inline style
41
+ // Shimmer and fade-in keyframes as inline style
42
42
  const shimmerKeyframes = `
43
43
  @keyframes sharesheet-shimmer {
44
44
  0% { transform: translateX(-100%); }
45
45
  100% { transform: translateX(100%); }
46
46
  }
47
+ @keyframes sharesheet-fadein {
48
+ 0% { opacity: 0; }
49
+ 100% { opacity: 1; }
50
+ }
51
+ .sharesheet-fadein {
52
+ animation: sharesheet-fadein 0.3s ease-out forwards;
53
+ }
47
54
  `;
48
55
 
49
56
  // Helper to create var() with fallback
@@ -57,12 +64,16 @@ export function ShareSheetContent({
57
64
  shareText,
58
65
  downloadUrl,
59
66
  downloadFilename,
67
+ previewImage,
68
+ shareFile,
69
+ shareFilename,
60
70
  className,
61
71
  classNames = {},
62
72
  buttonSize = DEFAULT_BUTTON_SIZE,
63
73
  iconSize = DEFAULT_ICON_SIZE,
64
74
  onNativeShare,
65
75
  onCopy,
76
+ onShare,
66
77
  onDownload,
67
78
  hide = [],
68
79
  show,
@@ -72,8 +83,8 @@ export function ShareSheetContent({
72
83
  const [imageLoaded, setImageLoaded] = useState(false);
73
84
  const [imageError, setImageError] = useState(false);
74
85
 
75
- // Fetch OG data automatically from shareUrl
76
- const { ogData, loading: ogLoading } = useOGData(shareUrl);
86
+ // Only fetch OG data if previewImage is not provided
87
+ const { ogData, loading: ogLoading } = useOGData(previewImage ? undefined : shareUrl);
77
88
 
78
89
  const handleImageLoad = useCallback(() => {
79
90
  setImageLoaded(true);
@@ -89,6 +100,8 @@ export function ShareSheetContent({
89
100
  downloadUrl,
90
101
  downloadFilename,
91
102
  emailSubject: shareText,
103
+ shareFile,
104
+ shareFilename,
92
105
  onNativeShare,
93
106
  onCopy,
94
107
  onDownload,
@@ -144,11 +157,14 @@ export function ShareSheetContent({
144
157
  // Use CSS var with fallback to platform color
145
158
  bgColor: cssVar(PLATFORM_CSS_VARS[id], PLATFORM_COLORS[id].bg),
146
159
  textColor: PLATFORM_COLORS[id].text,
147
- onClick: shareActions[id],
160
+ onClick: () => {
161
+ onShare?.(id);
162
+ shareActions[id]();
163
+ },
148
164
  condition,
149
165
  };
150
166
  });
151
- }, [iconSize, labels, icons, dynamicLabels, shareActions, shareSheet.canNativeShare, shareSheet.platformAvailability, downloadUrl]);
167
+ }, [iconSize, labels, icons, dynamicLabels, shareActions, onShare, shareSheet.canNativeShare, shareSheet.platformAvailability, downloadUrl]);
152
168
 
153
169
  const visibleButtons = useMemo(() => {
154
170
  // If show is provided, use its order
@@ -177,11 +193,13 @@ export function ShareSheetContent({
177
193
 
178
194
  // Render OG preview
179
195
  const renderPreview = () => {
180
- const ogImage = ogData?.image;
196
+ // Use previewImage if provided, otherwise use OG data
197
+ const ogImage = previewImage || ogData?.image;
181
198
  const hasImage = ogImage && !imageError;
199
+ const isLoading = !previewImage && ogLoading;
182
200
 
183
- // Loading state
184
- if (ogLoading) {
201
+ // Loading state (only when fetching OG data, not when previewImage is provided)
202
+ if (isLoading) {
185
203
  return (
186
204
  <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
187
205
  <div
@@ -214,8 +232,8 @@ export function ShareSheetContent({
214
232
  );
215
233
  }
216
234
 
217
- // No OG data or no image - show link placeholder
218
- if (!ogData || !hasImage) {
235
+ // No image available - show link placeholder (skip if previewImage was provided)
236
+ if (!hasImage) {
219
237
  return (
220
238
  <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
221
239
  <div
@@ -267,7 +285,27 @@ export function ShareSheetContent({
267
285
  );
268
286
  }
269
287
 
270
- // Image loading state
288
+ // If previewImage is a data URL, show it directly (no preloading needed)
289
+ const isDataUrl = typeof ogImage === "string" && ogImage.startsWith("data:");
290
+ if (isDataUrl) {
291
+ return (
292
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
293
+ {/* eslint-disable-next-line @next/next/no-img-element */}
294
+ <img
295
+ src={ogImage}
296
+ alt="Preview"
297
+ className={cn(defaultClasses.previewImage, classNames.previewImage)}
298
+ style={{
299
+ width: "100%",
300
+ maxWidth: "320px",
301
+ height: "auto",
302
+ }}
303
+ />
304
+ </div>
305
+ );
306
+ }
307
+
308
+ // Image loading state (for remote URLs)
271
309
  if (!imageLoaded) {
272
310
  return (
273
311
  <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
@@ -301,7 +339,7 @@ export function ShareSheetContent({
301
339
  {/* eslint-disable-next-line @next/next/no-img-element */}
302
340
  <img
303
341
  src={ogImage}
304
- alt={ogData.title || "Preview"}
342
+ alt={ogData?.title || "Preview"}
305
343
  onLoad={handleImageLoad}
306
344
  onError={handleImageError}
307
345
  style={{ display: "none" }}
@@ -310,21 +348,18 @@ export function ShareSheetContent({
310
348
  );
311
349
  }
312
350
 
313
- // Image loaded - show it
351
+ // Image loaded - show it with fade-in
314
352
  return (
315
353
  <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
316
354
  {/* eslint-disable-next-line @next/next/no-img-element */}
317
355
  <img
318
356
  src={ogImage}
319
- alt={ogData.title || "Preview"}
320
- className={cn(defaultClasses.previewImage, classNames.previewImage)}
357
+ alt={ogData?.title || "Preview"}
358
+ className={cn(defaultClasses.previewImage, classNames.previewImage, "sharesheet-fadein")}
321
359
  style={{
322
360
  width: "100%",
323
361
  maxWidth: "320px",
324
362
  height: "auto",
325
- borderRadius: "12px",
326
- opacity: 1,
327
- transition: "opacity 0.3s ease-in-out",
328
363
  }}
329
364
  />
330
365
  </div>
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import React, { useState } from "react";
4
4
  import { Drawer } from "vaul";
5
5
 
6
6
  import { cn } from "./utils";
@@ -21,12 +21,28 @@ function cssVar(name: string, fallback: string): string {
21
21
  return `var(${name}, ${fallback})`;
22
22
  }
23
23
 
24
+ // Screen reader only styles (inline to avoid Tailwind dependency)
25
+ const srOnlyStyle: React.CSSProperties = {
26
+ position: "absolute",
27
+ width: "1px",
28
+ height: "1px",
29
+ padding: 0,
30
+ margin: "-1px",
31
+ overflow: "hidden",
32
+ clip: "rect(0, 0, 0, 0)",
33
+ whiteSpace: "nowrap",
34
+ borderWidth: 0,
35
+ };
36
+
24
37
  export function ShareSheetDrawer({
25
38
  title = "Share",
26
39
  shareUrl,
27
40
  shareText,
28
41
  downloadUrl,
29
42
  downloadFilename,
43
+ previewImage,
44
+ shareFile,
45
+ shareFilename,
30
46
  disabled,
31
47
  children,
32
48
  open: controlledOpen,
@@ -78,7 +94,7 @@ export function ShareSheetDrawer({
78
94
  borderColor: cssVar(CSS_VARS_UI.drawerBorder, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.drawerBorder]),
79
95
  }}
80
96
  >
81
- <Drawer.Title className="sr-only">{title}</Drawer.Title>
97
+ <Drawer.Title style={srOnlyStyle}>{title}</Drawer.Title>
82
98
  <div
83
99
  className={cn(defaultDrawerClasses.drawerInner, classNames.drawerInner)}
84
100
  style={{
@@ -98,6 +114,9 @@ export function ShareSheetDrawer({
98
114
  shareText={shareText}
99
115
  downloadUrl={downloadUrl}
100
116
  downloadFilename={downloadFilename}
117
+ previewImage={previewImage}
118
+ shareFile={shareFile}
119
+ shareFilename={shareFilename}
101
120
  className={className}
102
121
  classNames={classNames}
103
122
  buttonSize={buttonSize}
package/src/hooks.ts CHANGED
@@ -50,6 +50,10 @@ export interface UseShareSheetOptions {
50
50
  downloadFilename?: string;
51
51
  /** Email subject (optional) */
52
52
  emailSubject?: string;
53
+ /** File to share via native share (data URL or Blob) - takes priority over URL sharing */
54
+ shareFile?: string | Blob | null;
55
+ /** Filename for the shared file */
56
+ shareFilename?: string;
53
57
  /** Callback after native share */
54
58
  onNativeShare?: () => void;
55
59
  /** Callback after copy */
@@ -68,6 +72,8 @@ export function useShareSheet({
68
72
  downloadUrl,
69
73
  downloadFilename,
70
74
  emailSubject = "Share",
75
+ shareFile,
76
+ shareFilename = "share.png",
71
77
  onNativeShare,
72
78
  onCopy,
73
79
  onDownload,
@@ -105,12 +111,64 @@ export function useShareSheet({
105
111
  }, [safeUrl, onCopy]);
106
112
 
107
113
  const nativeShare = useCallback(async () => {
108
- if (!safeUrl) return;
109
114
  const nav = navigator as Navigator & {
110
115
  share?: (data: ShareData) => Promise<void>;
116
+ canShare?: (data: ShareData) => boolean;
111
117
  };
112
118
  if (!("share" in nav) || typeof nav.share !== "function") return;
119
+
113
120
  try {
121
+ // If shareFile is provided, try to share as a file
122
+ if (shareFile) {
123
+ let blob: Blob;
124
+
125
+ // Convert data URL to Blob if needed
126
+ if (typeof shareFile === "string" && shareFile.startsWith("data:")) {
127
+ const response = await fetch(shareFile);
128
+ blob = await response.blob();
129
+ } else if (shareFile instanceof Blob) {
130
+ blob = shareFile;
131
+ } else {
132
+ // Invalid shareFile, fall back to URL sharing
133
+ if (!safeUrl) return;
134
+ await nav.share({
135
+ title: shareText,
136
+ text: shareText,
137
+ url: safeUrl,
138
+ });
139
+ onNativeShare?.();
140
+ return;
141
+ }
142
+
143
+ // Create a File from the Blob
144
+ const file = new File([blob], shareFilename, { type: blob.type || "image/png" });
145
+
146
+ // Check if file sharing is supported
147
+ const shareData: ShareData = {
148
+ files: [file],
149
+ title: shareText,
150
+ text: shareText,
151
+ };
152
+
153
+ if (nav.canShare && !nav.canShare(shareData)) {
154
+ // File sharing not supported, fall back to URL sharing
155
+ if (!safeUrl) return;
156
+ await nav.share({
157
+ title: shareText,
158
+ text: shareText,
159
+ url: safeUrl,
160
+ });
161
+ onNativeShare?.();
162
+ return;
163
+ }
164
+
165
+ await nav.share(shareData);
166
+ onNativeShare?.();
167
+ return;
168
+ }
169
+
170
+ // Fall back to URL sharing
171
+ if (!safeUrl) return;
114
172
  await nav.share({
115
173
  title: shareText,
116
174
  text: shareText,
@@ -120,7 +178,7 @@ export function useShareSheet({
120
178
  } catch {
121
179
  // user canceled or share failed -> ignore
122
180
  }
123
- }, [safeUrl, shareText, onNativeShare]);
181
+ }, [safeUrl, shareText, shareFile, shareFilename, onNativeShare]);
124
182
 
125
183
  const downloadFile = useCallback(async () => {
126
184
  const url = (downloadUrl ?? "").trim();
package/src/types.ts CHANGED
@@ -21,7 +21,7 @@ export const CSS_VAR_UI_DEFAULTS = {
21
21
  [CSS_VARS_UI.overlayBg]: "rgba(0, 0, 0, 0.7)",
22
22
  [CSS_VARS_UI.drawerBg]: "#09090b",
23
23
  [CSS_VARS_UI.drawerBorder]: "#27272a",
24
- [CSS_VARS_UI.handleBg]: "#27272a",
24
+ [CSS_VARS_UI.handleBg]: "rgba(255, 255, 255, 0.2)",
25
25
  [CSS_VARS_UI.titleColor]: "#ffffff",
26
26
  [CSS_VARS_UI.subtitleColor]: "#a1a1aa",
27
27
  [CSS_VARS_UI.buttonLabelColor]: "#ffffff",
@@ -108,6 +108,12 @@ export interface ShareSheetContentProps {
108
108
  downloadUrl?: string | null;
109
109
  /** Filename for downloaded file */
110
110
  downloadFilename?: string;
111
+ /** Custom preview image URL (skips OG fetching if provided) */
112
+ previewImage?: string | null;
113
+ /** File to share via native share (data URL or Blob) - takes priority over URL sharing */
114
+ shareFile?: string | Blob | null;
115
+ /** Filename for the shared file */
116
+ shareFilename?: string;
111
117
  /** Custom class name for the container (shorthand for classNames.root) */
112
118
  className?: string;
113
119
  /** Override class names for sub-components */
@@ -122,6 +128,8 @@ export interface ShareSheetContentProps {
122
128
  onCopy?: () => void;
123
129
  /** Called when download starts */
124
130
  onDownload?: () => void;
131
+ /** Called on every share option click, with the option id (analytics-friendly) */
132
+ onShare?: (option: ShareOption) => void;
125
133
  /** Hide specific share options */
126
134
  hide?: ShareOption[];
127
135
  /** Show only these platforms, in this exact order */