react-sharesheet 1.0.0 → 1.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.
Files changed (41) hide show
  1. package/README.md +40 -53
  2. package/dist/content.d.mts +3 -3
  3. package/dist/content.d.ts +3 -3
  4. package/dist/content.js +192 -268
  5. package/dist/content.js.map +1 -1
  6. package/dist/content.mjs +194 -270
  7. package/dist/content.mjs.map +1 -1
  8. package/dist/drawer.d.mts +3 -3
  9. package/dist/drawer.d.ts +3 -3
  10. package/dist/drawer.js +194 -272
  11. package/dist/drawer.js.map +1 -1
  12. package/dist/drawer.mjs +196 -274
  13. package/dist/drawer.mjs.map +1 -1
  14. package/dist/headless.d.mts +22 -3
  15. package/dist/headless.d.ts +22 -3
  16. package/dist/headless.js +72 -0
  17. package/dist/headless.js.map +1 -1
  18. package/dist/headless.mjs +70 -1
  19. package/dist/headless.mjs.map +1 -1
  20. package/dist/index.d.mts +2 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +203 -272
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.mjs +202 -274
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/{platforms-DU1DVDFq.d.mts → platforms-CDJmSY8E.d.mts} +2 -19
  27. package/dist/{platforms-DU1DVDFq.d.ts → platforms-CDJmSY8E.d.ts} +2 -19
  28. package/package.json +12 -3
  29. package/src/ShareSheetContent.tsx +143 -306
  30. package/src/ShareSheetDrawer.tsx +2 -4
  31. package/src/__tests__/hooks.test.ts +213 -0
  32. package/src/__tests__/og-fetcher.test.ts +144 -0
  33. package/src/__tests__/platforms.test.ts +148 -0
  34. package/src/__tests__/setup.ts +22 -0
  35. package/src/__tests__/share-functions.test.ts +155 -0
  36. package/src/__tests__/utils.test.ts +64 -0
  37. package/src/headless.ts +4 -1
  38. package/src/hooks.ts +49 -1
  39. package/src/index.ts +4 -3
  40. package/src/og-fetcher.ts +64 -0
  41. package/src/types.ts +1 -20
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  import { useMemo, useState, useCallback } from "react";
4
- import { Image, FileText, Music, Film, Link2, Play } from "lucide-react";
4
+ import { Image, Link2 } from "lucide-react";
5
5
  import { cn } from "./utils";
6
- import { useShareSheet } from "./hooks";
6
+ import { useShareSheet, useOGData } from "./hooks";
7
7
  import {
8
8
  PLATFORM_IDS,
9
9
  PLATFORM_COLORS,
@@ -17,49 +17,11 @@ import {
17
17
  type ShareSheetContentProps,
18
18
  type ShareOption,
19
19
  type ShareButtonConfig,
20
- type PreviewConfig,
21
- type PreviewType,
22
20
  } from "./types";
23
21
 
24
22
  const DEFAULT_BUTTON_SIZE = 45;
25
23
  const DEFAULT_ICON_SIZE = 22;
26
24
 
27
- // File extension mappings
28
- const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "avif"];
29
- const VIDEO_EXTENSIONS = ["mp4", "webm", "mov", "avi", "mkv", "m4v", "ogv"];
30
- const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "wma"];
31
-
32
- // Detect content type from URL
33
- function detectPreviewType(url: string): PreviewType {
34
- try {
35
- const pathname = new URL(url, "http://localhost").pathname;
36
- const ext = pathname.split(".").pop()?.toLowerCase() || "";
37
-
38
- if (IMAGE_EXTENSIONS.includes(ext)) return "image";
39
- if (VIDEO_EXTENSIONS.includes(ext)) return "video";
40
- if (AUDIO_EXTENSIONS.includes(ext)) return "audio";
41
-
42
- // Check for common patterns
43
- if (url.includes("/api/og") || url.includes("og-image")) return "image";
44
- if (url.includes("youtube.com") || url.includes("vimeo.com")) return "video";
45
-
46
- return "link";
47
- } catch {
48
- return "link";
49
- }
50
- }
51
-
52
- // Get filename from URL
53
- function getFilenameFromUrl(url: string): string {
54
- try {
55
- const pathname = new URL(url, "http://localhost").pathname;
56
- const filename = pathname.split("/").pop() || "";
57
- return decodeURIComponent(filename);
58
- } catch {
59
- return url;
60
- }
61
- }
62
-
63
25
  // Default class names
64
26
  const defaultClasses = {
65
27
  root: "max-w-md mx-auto",
@@ -69,12 +31,8 @@ const defaultClasses = {
69
31
  preview: "flex justify-center mb-4 px-4",
70
32
  previewSkeleton: "rounded-xl overflow-hidden",
71
33
  previewImage: "",
72
- previewVideo: "",
73
- previewFile: "",
74
- previewFileIcon: "",
75
- previewFilename: "truncate",
76
- previewLink: "",
77
- grid: "px-2 py-6 flex flex-row items-center gap-4 gap-y-6 flex-wrap justify-center",
34
+ previewMeta: "",
35
+ grid: "px-2 py-6 flex flex-row items-start gap-4 gap-y-6 flex-wrap justify-center",
78
36
  button: "flex flex-col items-center gap-0 text-xs w-[60px] outline-none cursor-pointer group",
79
37
  buttonIcon: "p-2 rounded-full transition-all flex items-center justify-center group-hover:scale-110 group-active:scale-95 mb-2",
80
38
  buttonLabel: "",
@@ -93,34 +51,10 @@ function cssVar(name: string, fallback: string): string {
93
51
  return `var(${name}, ${fallback})`;
94
52
  }
95
53
 
96
- // Normalize preview prop to PreviewConfig
97
- function normalizePreview(preview: string | PreviewConfig | null | undefined): PreviewConfig | null {
98
- if (!preview) return null;
99
-
100
- if (typeof preview === "string") {
101
- const type = detectPreviewType(preview);
102
- return {
103
- url: preview,
104
- type,
105
- filename: getFilenameFromUrl(preview),
106
- };
107
- }
108
-
109
- // It's already a config object
110
- const type = preview.type === "auto" || !preview.type ? detectPreviewType(preview.url) : preview.type;
111
-
112
- return {
113
- ...preview,
114
- type,
115
- filename: preview.filename || getFilenameFromUrl(preview.url),
116
- };
117
- }
118
-
119
54
  export function ShareSheetContent({
120
55
  title = "Share",
121
56
  shareUrl,
122
57
  shareText,
123
- preview,
124
58
  downloadUrl,
125
59
  downloadFilename,
126
60
  className,
@@ -135,19 +69,19 @@ export function ShareSheetContent({
135
69
  labels = {},
136
70
  icons = {},
137
71
  }: ShareSheetContentProps) {
138
- const [mediaLoaded, setMediaLoaded] = useState(false);
139
- const [mediaError, setMediaError] = useState(false);
72
+ const [imageLoaded, setImageLoaded] = useState(false);
73
+ const [imageError, setImageError] = useState(false);
140
74
 
141
- const handleMediaLoad = useCallback(() => {
142
- setMediaLoaded(true);
143
- }, []);
75
+ // Fetch OG data automatically from shareUrl
76
+ const { ogData, loading: ogLoading } = useOGData(shareUrl);
144
77
 
145
- const handleMediaError = useCallback(() => {
146
- setMediaError(true);
78
+ const handleImageLoad = useCallback(() => {
79
+ setImageLoaded(true);
147
80
  }, []);
148
81
 
149
- // Normalize preview config
150
- const previewConfig = useMemo(() => normalizePreview(preview), [preview]);
82
+ const handleImageError = useCallback(() => {
83
+ setImageError(true);
84
+ }, []);
151
85
 
152
86
  const shareSheet = useShareSheet({
153
87
  shareUrl,
@@ -219,60 +153,33 @@ export function ShareSheetContent({
219
153
  });
220
154
  }, [buttons, show, hide]);
221
155
 
222
- const showPreview = !!previewConfig;
156
+ const bgColor = cssVar(CSS_VARS_UI.previewBg, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.previewBg]);
157
+ const shimmerColor = cssVar(CSS_VARS_UI.previewShimmer, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.previewShimmer]);
158
+ const textColor = cssVar(CSS_VARS_UI.subtitleColor, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.subtitleColor]);
223
159
 
224
- // Render preview based on type
160
+ // Render OG preview
225
161
  const renderPreview = () => {
226
- if (!previewConfig) return null;
227
-
228
- const { type, url, filename, alt, poster } = previewConfig;
229
- const bgColor = cssVar(CSS_VARS_UI.previewBg, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.previewBg]);
230
- const shimmerColor = cssVar(CSS_VARS_UI.previewShimmer, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.previewShimmer]);
231
- const textColor = cssVar(CSS_VARS_UI.subtitleColor, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.subtitleColor]);
162
+ const ogImage = ogData?.image;
163
+ const hasImage = ogImage && !imageError;
232
164
 
233
- // Floating URL label (centered below)
234
- const UrlLabel = ({ displayUrl = url }: { displayUrl?: string }) => (
235
- <div
236
- className={cn(defaultClasses.previewFilename, classNames.previewFilename)}
237
- style={{
238
- color: textColor,
239
- fontSize: "10px",
240
- opacity: 0.5,
241
- textAlign: "center",
242
- marginTop: "6px",
243
- }}
244
- >
245
- {displayUrl}
246
- </div>
247
- );
248
-
249
- // Placeholder card for non-media types or loading/error states
250
- const PlaceholderCard = ({
251
- icon: IconComponent,
252
- isLoading = false,
253
- label,
254
- displayUrl,
255
- }: {
256
- icon: typeof Link2;
257
- isLoading?: boolean;
258
- label?: string;
259
- displayUrl?: string;
260
- }) => (
261
- <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
262
- <div
263
- className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
264
- style={{
265
- position: "relative",
266
- backgroundColor: bgColor,
267
- width: "200px",
268
- height: "120px",
269
- overflow: "hidden",
270
- display: "flex",
271
- alignItems: "center",
272
- justifyContent: "center",
273
- }}
274
- >
275
- {isLoading && (
165
+ // Loading state
166
+ if (ogLoading) {
167
+ return (
168
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
169
+ <div
170
+ className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
171
+ style={{
172
+ position: "relative",
173
+ backgroundColor: bgColor,
174
+ width: "100%",
175
+ maxWidth: "320px",
176
+ aspectRatio: "1.91 / 1",
177
+ overflow: "hidden",
178
+ display: "flex",
179
+ alignItems: "center",
180
+ justifyContent: "center",
181
+ }}
182
+ >
276
183
  <div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
277
184
  <div
278
185
  style={{
@@ -283,194 +190,127 @@ export function ShareSheetContent({
283
190
  }}
284
191
  />
285
192
  </div>
286
- )}
193
+ <Link2 size={32} style={{ color: textColor, opacity: 0.4 }} />
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ // No OG data or no image - show link placeholder
200
+ if (!ogData || !hasImage) {
201
+ return (
202
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
287
203
  <div
204
+ className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
288
205
  style={{
206
+ position: "relative",
207
+ backgroundColor: bgColor,
208
+ width: "100%",
209
+ maxWidth: "320px",
210
+ aspectRatio: "1.91 / 1",
211
+ overflow: "hidden",
289
212
  display: "flex",
290
- flexDirection: "column",
291
213
  alignItems: "center",
292
214
  justifyContent: "center",
293
- gap: "8px",
294
215
  }}
295
216
  >
296
- <IconComponent size={32} style={{ color: textColor, opacity: 0.4 }} />
297
- {label && (
298
- <span style={{ color: textColor, fontSize: "11px", opacity: 0.4 }}>
299
- {label}
300
- </span>
301
- )}
302
- </div>
303
- </div>
304
- <UrlLabel displayUrl={displayUrl} />
305
- </div>
306
- );
307
-
308
- // If there was an error loading media, show fallback
309
- if (mediaError && (type === "image" || type === "video")) {
310
- return <PlaceholderCard icon={Link2} displayUrl={url} />;
311
- }
312
-
313
- switch (type) {
314
- case "image":
315
- // Show placeholder while loading, then show image with correct aspect ratio
316
- if (!mediaLoaded) {
317
- return (
318
- <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
319
- <div
320
- className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
321
- style={{
322
- position: "relative",
323
- backgroundColor: bgColor,
324
- width: "200px",
325
- height: "120px",
326
- overflow: "hidden",
327
- display: "flex",
328
- alignItems: "center",
329
- justifyContent: "center",
330
- }}
331
- >
332
- <div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
333
- <div
334
- style={{
335
- position: "absolute",
336
- inset: 0,
337
- background: `linear-gradient(90deg, transparent, ${shimmerColor}, transparent)`,
338
- animation: "sharesheet-shimmer 1.5s infinite",
339
- }}
340
- />
341
- </div>
342
- <Image size={32} style={{ color: textColor, opacity: 0.4 }} />
343
- </div>
344
- <UrlLabel />
345
- {/* Hidden image for preloading */}
346
- {/* eslint-disable-next-line @next/next/no-img-element */}
347
- <img
348
- src={url}
349
- alt={alt || "Preview"}
350
- onLoad={handleMediaLoad}
351
- onError={handleMediaError}
352
- style={{ display: "none" }}
353
- />
354
- </div>
355
- );
356
- }
357
- // Image loaded - show with correct aspect ratio
358
- return (
359
- <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
360
- {/* eslint-disable-next-line @next/next/no-img-element */}
361
- <img
362
- src={url}
363
- alt={alt || "Preview"}
364
- className={cn(defaultClasses.previewImage, classNames.previewImage)}
217
+ <div
365
218
  style={{
366
- maxWidth: "100%",
367
- maxHeight: "180px",
368
- borderRadius: "12px",
369
- opacity: 1,
370
- transition: "opacity 0.3s ease-in-out",
219
+ display: "flex",
220
+ flexDirection: "column",
221
+ alignItems: "center",
222
+ justifyContent: "center",
223
+ gap: "8px",
224
+ padding: "16px",
371
225
  }}
372
- />
373
- <UrlLabel />
226
+ >
227
+ <Link2 size={32} style={{ color: textColor, opacity: 0.4 }} />
228
+ {ogData?.title && (
229
+ <span
230
+ style={{
231
+ color: textColor,
232
+ fontSize: "12px",
233
+ opacity: 0.6,
234
+ textAlign: "center",
235
+ maxWidth: "280px",
236
+ overflow: "hidden",
237
+ textOverflow: "ellipsis",
238
+ display: "-webkit-box",
239
+ WebkitLineClamp: 2,
240
+ WebkitBoxOrient: "vertical",
241
+ }}
242
+ >
243
+ {ogData.title}
244
+ </span>
245
+ )}
246
+ </div>
374
247
  </div>
375
- );
248
+ </div>
249
+ );
250
+ }
376
251
 
377
- case "video":
378
- if (!mediaLoaded) {
379
- return (
380
- <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
381
- <div
382
- className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
383
- style={{
384
- position: "relative",
385
- backgroundColor: bgColor,
386
- width: "200px",
387
- height: "120px",
388
- overflow: "hidden",
389
- display: "flex",
390
- alignItems: "center",
391
- justifyContent: "center",
392
- }}
393
- >
394
- <div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
395
- <div
396
- style={{
397
- position: "absolute",
398
- inset: 0,
399
- background: `linear-gradient(90deg, transparent, ${shimmerColor}, transparent)`,
400
- animation: "sharesheet-shimmer 1.5s infinite",
401
- }}
402
- />
403
- </div>
404
- <Film size={32} style={{ color: textColor, opacity: 0.4 }} />
405
- </div>
406
- <UrlLabel />
407
- {/* Hidden video for preloading */}
408
- <video
409
- src={url}
410
- poster={poster}
411
- onLoadedData={handleMediaLoad}
412
- onError={handleMediaError}
413
- style={{ display: "none" }}
414
- muted
415
- playsInline
416
- preload="metadata"
417
- />
418
- </div>
419
- );
420
- }
421
- // Video loaded
422
- return (
423
- <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
424
- <div style={{ position: "relative", borderRadius: "12px", overflow: "hidden" }}>
425
- <video
426
- src={url}
427
- poster={poster}
428
- className={cn(defaultClasses.previewVideo, classNames.previewVideo)}
429
- style={{
430
- maxWidth: "100%",
431
- maxHeight: "180px",
432
- display: "block",
433
- }}
434
- muted
435
- playsInline
436
- preload="metadata"
437
- />
438
- {/* Play icon overlay */}
252
+ // Image loading state
253
+ if (!imageLoaded) {
254
+ return (
255
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
256
+ <div
257
+ className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
258
+ style={{
259
+ position: "relative",
260
+ backgroundColor: bgColor,
261
+ width: "100%",
262
+ maxWidth: "320px",
263
+ aspectRatio: "1.91 / 1",
264
+ overflow: "hidden",
265
+ display: "flex",
266
+ alignItems: "center",
267
+ justifyContent: "center",
268
+ }}
269
+ >
270
+ <div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
439
271
  <div
440
272
  style={{
441
273
  position: "absolute",
442
274
  inset: 0,
443
- display: "flex",
444
- alignItems: "center",
445
- justifyContent: "center",
446
- pointerEvents: "none",
275
+ background: `linear-gradient(90deg, transparent, ${shimmerColor}, transparent)`,
276
+ animation: "sharesheet-shimmer 1.5s infinite",
447
277
  }}
448
- >
449
- <div
450
- style={{
451
- backgroundColor: "rgba(0, 0, 0, 0.5)",
452
- borderRadius: "50%",
453
- padding: "10px",
454
- }}
455
- >
456
- <Play size={20} fill="white" color="white" />
457
- </div>
458
- </div>
278
+ />
459
279
  </div>
460
- <UrlLabel />
280
+ <Image size={32} style={{ color: textColor, opacity: 0.4 }} />
461
281
  </div>
462
- );
463
-
464
- case "audio":
465
- return <PlaceholderCard icon={Music} label={filename || "Audio"} displayUrl={url} />;
466
-
467
- case "file":
468
- return <PlaceholderCard icon={FileText} label={filename || "File"} displayUrl={url} />;
469
-
470
- case "link":
471
- default:
472
- return <PlaceholderCard icon={Link2} displayUrl={url} />;
282
+ {/* Hidden image for preloading */}
283
+ {/* eslint-disable-next-line @next/next/no-img-element */}
284
+ <img
285
+ src={ogImage}
286
+ alt={ogData.title || "Preview"}
287
+ onLoad={handleImageLoad}
288
+ onError={handleImageError}
289
+ style={{ display: "none" }}
290
+ />
291
+ </div>
292
+ );
473
293
  }
294
+
295
+ // Image loaded - show it
296
+ return (
297
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: "100%" }}>
298
+ {/* eslint-disable-next-line @next/next/no-img-element */}
299
+ <img
300
+ src={ogImage}
301
+ alt={ogData.title || "Preview"}
302
+ className={cn(defaultClasses.previewImage, classNames.previewImage)}
303
+ style={{
304
+ width: "100%",
305
+ maxWidth: "320px",
306
+ height: "auto",
307
+ borderRadius: "12px",
308
+ opacity: 1,
309
+ transition: "opacity 0.3s ease-in-out",
310
+ }}
311
+ />
312
+ </div>
313
+ );
474
314
  };
475
315
 
476
316
  return (
@@ -493,12 +333,10 @@ export function ShareSheetContent({
493
333
  </div>
494
334
  </div>
495
335
 
496
- {/* Content Preview */}
497
- {showPreview && (
498
- <div className={cn(defaultClasses.preview, classNames.preview)}>
499
- {renderPreview()}
500
- </div>
501
- )}
336
+ {/* OG Preview - always shown */}
337
+ <div className={cn(defaultClasses.preview, classNames.preview)}>
338
+ {renderPreview()}
339
+ </div>
502
340
 
503
341
  <div className={cn(defaultClasses.grid, classNames.grid)}>
504
342
  {visibleButtons.map((btn) => (
@@ -535,4 +373,3 @@ export function ShareSheetContent({
535
373
  // Legacy export for backwards compatibility
536
374
  /** @deprecated Use ShareSheetContent instead */
537
375
  export const ShareMenuContent = ShareSheetContent;
538
-
@@ -10,8 +10,8 @@ import { CSS_VARS_UI, CSS_VAR_UI_DEFAULTS, type ShareSheetDrawerProps } from "./
10
10
  // Default class names for drawer
11
11
  const defaultDrawerClasses = {
12
12
  overlay: "fixed inset-0 z-[70]",
13
- drawer: "flex flex-col rounded-t-[14px] h-[70%] mt-24 fixed bottom-0 left-0 right-0 z-[80] border-t outline-none",
14
- drawerInner: "p-4 rounded-t-[14px] flex-1 overflow-auto",
13
+ drawer: "flex flex-col rounded-t-[14px] max-h-[90%] fixed bottom-0 left-0 right-0 z-[80] border-t outline-none",
14
+ drawerInner: "p-4 pb-8 rounded-t-[14px] overflow-auto",
15
15
  handle: "mx-auto w-12 h-1.5 shrink-0 rounded-full mb-6",
16
16
  trigger: "",
17
17
  };
@@ -25,7 +25,6 @@ export function ShareSheetDrawer({
25
25
  title = "Share",
26
26
  shareUrl,
27
27
  shareText,
28
- preview,
29
28
  downloadUrl,
30
29
  downloadFilename,
31
30
  disabled,
@@ -97,7 +96,6 @@ export function ShareSheetDrawer({
97
96
  title={title}
98
97
  shareUrl={shareUrl}
99
98
  shareText={shareText}
100
- preview={preview}
101
99
  downloadUrl={downloadUrl}
102
100
  downloadFilename={downloadFilename}
103
101
  className={className}