@curvenote/renderers 1.0.1 → 2.0.1

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 (105) hide show
  1. package/dist/articles.d.ts.map +1 -1
  2. package/dist/articles.js +8 -2
  3. package/dist/components/admonition.d.ts.map +1 -1
  4. package/dist/components/admonition.js +24 -4
  5. package/dist/components/anywidget-host.d.ts +7 -0
  6. package/dist/components/anywidget-host.d.ts.map +1 -0
  7. package/dist/components/anywidget-host.js +11 -0
  8. package/dist/components/bluesky.d.ts +6 -0
  9. package/dist/components/bluesky.d.ts.map +1 -0
  10. package/dist/components/bluesky.js +113 -0
  11. package/dist/components/cite-figurebar.d.ts +2 -1
  12. package/dist/components/cite-figurebar.d.ts.map +1 -1
  13. package/dist/components/cite-figurebar.js +73 -27
  14. package/dist/components/cite.d.ts +11 -0
  15. package/dist/components/cite.d.ts.map +1 -1
  16. package/dist/components/cite.js +130 -8
  17. package/dist/components/copy-icon.d.ts +11 -0
  18. package/dist/components/copy-icon.d.ts.map +1 -0
  19. package/dist/components/copy-icon.js +32 -0
  20. package/dist/components/definition-list.d.ts.map +1 -1
  21. package/dist/components/definition-list.js +3 -2
  22. package/dist/components/faq.js +1 -1
  23. package/dist/components/hash-link.d.ts +3 -0
  24. package/dist/components/hash-link.d.ts.map +1 -0
  25. package/dist/components/hash-link.js +3 -0
  26. package/dist/components/hero.d.ts +1 -1
  27. package/dist/components/hero.d.ts.map +1 -1
  28. package/dist/components/hero.js +14 -2
  29. package/dist/components/hover-popover.d.ts +26 -0
  30. package/dist/components/hover-popover.d.ts.map +1 -0
  31. package/dist/components/hover-popover.js +26 -0
  32. package/dist/components/images.d.ts.map +1 -1
  33. package/dist/components/images.js +7 -2
  34. package/dist/components/index.d.ts +3 -0
  35. package/dist/components/index.d.ts.map +1 -1
  36. package/dist/components/index.js +3 -0
  37. package/dist/components/pdb.d.ts.map +1 -1
  38. package/dist/components/pdb.js +42 -6
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +11 -3
  42. package/dist/myst/abbreviation.d.ts +6 -0
  43. package/dist/myst/abbreviation.d.ts.map +1 -0
  44. package/dist/myst/abbreviation.js +13 -0
  45. package/dist/myst/anywidget.d.ts +5 -0
  46. package/dist/myst/anywidget.d.ts.map +1 -0
  47. package/dist/myst/anywidget.js +12 -0
  48. package/dist/myst/code.d.ts +6 -0
  49. package/dist/myst/code.d.ts.map +1 -0
  50. package/dist/myst/code.js +103 -0
  51. package/dist/myst/container.d.ts +7 -0
  52. package/dist/myst/container.d.ts.map +1 -0
  53. package/dist/myst/container.js +57 -0
  54. package/dist/myst/cross-reference.d.ts +23 -0
  55. package/dist/myst/cross-reference.d.ts.map +1 -0
  56. package/dist/myst/cross-reference.js +140 -0
  57. package/dist/myst/footnotes.d.ts +7 -0
  58. package/dist/myst/footnotes.d.ts.map +1 -0
  59. package/dist/myst/footnotes.js +32 -0
  60. package/dist/myst/index.d.ts +14 -0
  61. package/dist/myst/index.d.ts.map +1 -0
  62. package/dist/myst/index.js +17 -0
  63. package/dist/myst/inline-expression.d.ts +6 -0
  64. package/dist/myst/inline-expression.d.ts.map +1 -0
  65. package/dist/myst/inline-expression.js +20 -0
  66. package/dist/myst/links/figshare.d.ts +8 -0
  67. package/dist/myst/links/figshare.d.ts.map +1 -0
  68. package/dist/myst/links/figshare.js +45 -0
  69. package/dist/myst/links/geo.d.ts +8 -0
  70. package/dist/myst/links/geo.d.ts.map +1 -0
  71. package/dist/myst/links/geo.js +116 -0
  72. package/dist/myst/links/github.d.ts +15 -0
  73. package/dist/myst/links/github.d.ts.map +1 -0
  74. package/dist/myst/links/github.js +156 -0
  75. package/dist/myst/links/huggingface.d.ts +10 -0
  76. package/dist/myst/links/huggingface.d.ts.map +1 -0
  77. package/dist/myst/links/huggingface.js +73 -0
  78. package/dist/myst/links/index.d.ts +12 -0
  79. package/dist/myst/links/index.d.ts.map +1 -0
  80. package/dist/myst/links/index.js +95 -0
  81. package/dist/myst/links/ror.d.ts +7 -0
  82. package/dist/myst/links/ror.d.ts.map +1 -0
  83. package/dist/myst/links/ror.js +34 -0
  84. package/dist/myst/links/rrid.d.ts +5 -0
  85. package/dist/myst/links/rrid.d.ts.map +1 -0
  86. package/dist/myst/links/rrid.js +31 -0
  87. package/dist/myst/links/wiki.d.ts +9 -0
  88. package/dist/myst/links/wiki.d.ts.map +1 -0
  89. package/dist/myst/links/wiki.js +41 -0
  90. package/dist/myst/math.d.ts +15 -0
  91. package/dist/myst/math.d.ts.map +1 -0
  92. package/dist/myst/math.js +53 -0
  93. package/dist/transforms/articles.d.ts +2 -1
  94. package/dist/transforms/articles.d.ts.map +1 -1
  95. package/dist/transforms/articles.js +2 -2
  96. package/dist/transforms/index.d.ts +5 -2
  97. package/dist/transforms/index.d.ts.map +1 -1
  98. package/dist/transforms/index.js +1 -0
  99. package/dist/utils/anywidget-analytics.d.ts +13 -0
  100. package/dist/utils/anywidget-analytics.d.ts.map +1 -0
  101. package/dist/utils/anywidget-analytics.js +15 -0
  102. package/dist/utils/content-analytics.d.ts +44 -0
  103. package/dist/utils/content-analytics.d.ts.map +1 -0
  104. package/dist/utils/content-analytics.js +59 -0
  105. package/package.json +22 -13
@@ -1 +1 @@
1
- {"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["../src/articles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAoC1D,eAAO,MAAM,kBAAkB;;CAA0C,CAAC"}
1
+ {"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["../src/articles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAgD1D,eAAO,MAAM,kBAAkB;;CAA0C,CAAC"}
package/dist/articles.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArticleCard } from '@curvenote/theme-ui';
2
+ import { ArticleCard, useAnalytics } from '@curvenote/theme-ui';
3
3
  const ArticlesRenderer = ({ node }) => {
4
- return (_jsxs("div", { children: [!node.error && (_jsx("div", { className: "space-y-6", children: node.items.map((article) => (_jsx(ArticleCard, { to: `/articles/${article.slug ?? article.id}`, article: article, showThumbnails: node['show-thumbnails'] ?? false, showAuthors: node['show-authors'] ?? false, showDoi: node['show-doi'] ?? false, showDate: node['show-date'] ?? false }, article.id))) })), !node.error && node.items.length === 0 && (_jsx("div", { className: "px-2 py-4 text-gray-600 border-gray-500 border-gray-[1px] rounded-sm bg-gray-50", children: "There are no articles in this listing yet." })), node.error && (_jsx("div", { className: "text-red-500 p-4 my-2 bg-red-50 border-[1px] border-red-500 rounded-sm", children: node.error }))] }));
4
+ const capture = useAnalytics();
5
+ return (_jsxs("div", { children: [!node.error && (_jsx("div", { className: "space-y-6", children: node.items.map((article) => (_jsx("div", { onClick: () => capture('content_article_card_clicked', {
6
+ surface: 'content',
7
+ articleId: article.id,
8
+ articleSlug: article.slug,
9
+ articleTitle: article.title,
10
+ }), children: _jsx(ArticleCard, { to: `/articles/${article.slug ?? article.id}`, article: article, showThumbnails: node['show-thumbnails'] ?? false, showAuthors: node['show-authors'] ?? false, showDoi: node['show-doi'] ?? false, showDate: node['show-date'] ?? false }) }, article.id))) })), !node.error && node.items.length === 0 && (_jsx("div", { className: "px-2 py-4 text-gray-600 border-gray-500 border-gray-[1px] rounded-sm bg-gray-50", children: "There are no articles in this listing yet." })), node.error && (_jsx("div", { className: "text-red-500 p-4 my-2 bg-red-50 border-[1px] border-red-500 rounded-sm", children: node.error }))] }));
5
11
  };
6
12
  export const ARTICLES_RENDERERS = { curvenoteArticles: ArticlesRenderer };
@@ -1 +1 @@
1
- {"version":3,"file":"admonition.d.ts","sourceRoot":"","sources":["../../src/components/admonition.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AA8B1D,oBAAY,cAAc;IACxB,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,KAAK,UAAU;IACf,SAAS,cAAc;IACvB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,GAAG,QAAQ;IACX,OAAO,YAAY;CACpB;AA8PD,eAAO,MAAM,oBAAoB;;CAAqC,CAAC"}
1
+ {"version":3,"file":"admonition.d.ts","sourceRoot":"","sources":["../../src/components/admonition.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AA+B1D,oBAAY,cAAc;IACxB,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,KAAK,UAAU;IACf,SAAS,cAAc;IACvB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,GAAG,QAAQ;IACX,OAAO,YAAY;CACpB;AAoRD,eAAO,MAAM,oBAAoB;;CAAqC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Collapsible, CollapsibleContent, CollapsibleTrigger, Button, SimpleTooltip, } from '@curvenote/theme-ui';
2
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger, Button, SimpleTooltip, useAnalytics, } from '@curvenote/theme-ui';
3
3
  import { ChevronRight, Link as LinkIcon, Info, AlertTriangle, AlertCircle, XCircle, Megaphone, Lightbulb, Zap, ArrowRight, SquarePen, } from 'lucide-react';
4
4
  import { useState } from 'react';
5
5
  import { MyST } from 'myst-to-react';
@@ -54,7 +54,7 @@ const admonitionVariants = cva('border border-muted border-l-2 bg-muted/30', {
54
54
  variant: 'default',
55
55
  },
56
56
  });
57
- const iconVariants = cva('w-5 h-5 mt-0.5 flex-shrink-0 self-center', {
57
+ const iconVariants = cva('w-5 h-5 mt-0.5 shrink-0 self-center', {
58
58
  variants: {
59
59
  color: {
60
60
  blue: 'text-blue-600 dark:text-blue-400',
@@ -108,16 +108,28 @@ function getFirstKind({ kind, classes = [], }) {
108
108
  return ADMONITION_CONFIGS[AdmonitionKind.note];
109
109
  }
110
110
  const AdmonitionComponent = ({ id, title, content, defaultOpen, config, isDropdown, hideIcon, className, }) => {
111
+ const capture = useAnalytics();
111
112
  const [isOpen, setIsOpen] = useState(defaultOpen);
112
113
  const IconComponent = config.icon;
113
114
  const handleCopyLink = async () => {
114
115
  const url = `${window.location.origin}${window.location.pathname}#${id}`;
115
116
  try {
116
117
  await navigator.clipboard.writeText(url);
117
- toast.success('Link copied to clipboard');
118
118
  }
119
119
  catch (err) {
120
120
  console.error('Failed to copy link:', err);
121
+ return;
122
+ }
123
+ toast.success('Link copied to clipboard');
124
+ try {
125
+ capture('content_admonition_link_copied', {
126
+ surface: 'content',
127
+ admonitionKind: config.kind,
128
+ targetId: id,
129
+ });
130
+ }
131
+ catch {
132
+ // best-effort analytics — errors must not surface to the user here
121
133
  }
122
134
  };
123
135
  if (!isDropdown) {
@@ -126,7 +138,15 @@ const AdmonitionComponent = ({ id, title, content, defaultOpen, config, isDropdo
126
138
  return (_jsx("div", { className: "overflow-hidden my-4 rounded-xs", children: _jsx("div", { className: cn(admonitionVariants({
127
139
  color: config.color,
128
140
  variant: 'collapsible',
129
- }), className), children: _jsxs(Collapsible, { open: isOpen, onOpenChange: setIsOpen, children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { className: "flex justify-between items-start p-4 w-full text-left cursor-pointer", children: [_jsxs("div", { className: "flex gap-3 items-start", children: [_jsx("div", { className: "flex-shrink-0 mt-1.5", children: _jsx(ChevronRight, { className: cn('w-4 h-4 transition-transform duration-200', {
141
+ }), className), children: _jsxs(Collapsible, { open: isOpen, onOpenChange: (open) => {
142
+ setIsOpen(open);
143
+ capture('content_admonition_toggled', {
144
+ surface: 'content',
145
+ admonitionKind: config.kind,
146
+ targetId: id,
147
+ open,
148
+ });
149
+ }, children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { className: "flex justify-between items-start p-4 w-full text-left cursor-pointer", children: [_jsxs("div", { className: "flex gap-3 items-start", children: [_jsx("div", { className: "shrink-0 mt-1.5", children: _jsx(ChevronRight, { className: cn('w-4 h-4 transition-transform duration-200', {
130
150
  'rotate-90': isOpen,
131
151
  }) }) }), _jsxs("div", { className: "flex gap-3 items-start", children: [!hideIcon && (_jsx(IconComponent, { className: iconVariants({ color: config.color }) })), _jsx("span", { className: "font-medium text-foreground", children: title })] })] }), _jsx(SimpleTooltip, { title: "Copy link", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => {
132
152
  e.stopPropagation();
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type AnywidgetIdentity } from '../utils/anywidget-analytics';
3
+ export declare function AnywidgetClickHost({ children, identity, }: {
4
+ children: ReactNode;
5
+ identity: AnywidgetIdentity;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=anywidget-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anywidget-host.d.ts","sourceRoot":"","sources":["../../src/components/anywidget-host.tsx"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAE/F,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,iBAAiB,CAAC;CAC7B,2CAYA"}
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAnalytics } from '@curvenote/theme-ui';
3
+ import { useCallback } from 'react';
4
+ import { captureAnywidgetClicked } from '../utils/anywidget-analytics';
5
+ export function AnywidgetClickHost({ children, identity, }) {
6
+ const capture = useAnalytics();
7
+ const handleClick = useCallback(() => {
8
+ captureAnywidgetClicked(capture, identity);
9
+ }, [capture, identity]);
10
+ return (_jsx("div", { className: "relative w-full", style: { position: 'relative' }, onClick: handleClick, children: children }));
11
+ }
@@ -0,0 +1,6 @@
1
+ import type { GenericNode } from 'myst-common';
2
+ import type { NodeRenderer, NodeRenderers } from '@myst-theme/providers';
3
+ export declare const BlueskyBlockRenderer: NodeRenderer<GenericNode>;
4
+ export declare const BlueskyLinkRenderer: NodeRenderer<GenericNode>;
5
+ export declare const BLUESKY_LINK_RENDERERS: NodeRenderers;
6
+ //# sourceMappingURL=bluesky.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bluesky.d.ts","sourceRoot":"","sources":["../../src/components/bluesky.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAsNzE,eAAO,MAAM,oBAAoB,EAAE,YAAY,CAAC,WAAW,CAI1D,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,YAAY,CAAC,WAAW,CAsCzD,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,aAOpC,CAAC"}
@@ -0,0 +1,113 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { default as useSWR } from 'swr';
3
+ import { MyST } from 'myst-to-react';
4
+ import { HoverPopover } from './hover-popover';
5
+ import { useAnalytics } from '@curvenote/theme-ui';
6
+ import { useCallback } from 'react';
7
+ import { captureContentLinkClicked, useContentEnrichmentDisplay, } from '../utils/content-analytics';
8
+ // ─── Bluesky logo SVG ──────────────────────────────────────────────────────────
9
+ // Official logomark from github.com/bluesky-social/social-app
10
+ function BskyIcon() {
11
+ return (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", xmlns: "http://www.w3.org/2000/svg", className: "flex-shrink-0", children: _jsx("path", { d: "M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z" }) }));
12
+ }
13
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
14
+ function fmt(n) {
15
+ if (n == null)
16
+ return '0';
17
+ if (n >= 1_000_000)
18
+ return `${(n / 1_000_000).toFixed(1)}M`;
19
+ if (n >= 1_000)
20
+ return `${(n / 1_000).toFixed(1)}K`;
21
+ return String(n);
22
+ }
23
+ const profileFetcher = (url) => fetch(url).then(async (res) => {
24
+ if (!res.ok) {
25
+ const body = await res.json().catch(() => ({}));
26
+ throw new Error(body.message || res.statusText);
27
+ }
28
+ return res.json();
29
+ });
30
+ // ─── Profile card ─────────────────────────────────────────────────────────────
31
+ function BlueskyProfileCard({ handle, engagementSurface = 'hover_card', }) {
32
+ const capture = useAnalytics();
33
+ const apiUrl = `https://api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`;
34
+ const { data, error } = useSWR(apiUrl, profileFetcher, {
35
+ revalidateOnFocus: false,
36
+ revalidateOnReconnect: false,
37
+ dedupingInterval: 60_000,
38
+ errorRetryCount: 2,
39
+ errorRetryInterval: 1_000,
40
+ });
41
+ const profileUrl = `https://bsky.app/profile/${handle}`;
42
+ const captureProfileLinkClick = useCallback(() => {
43
+ captureContentLinkClicked(capture, {
44
+ linkKind: 'bluesky_profile',
45
+ destinationUrl: profileUrl,
46
+ engagementSurface,
47
+ cardKind: 'bluesky_profile',
48
+ properties: { handle },
49
+ });
50
+ }, [capture, engagementSurface, handle, profileUrl]);
51
+ const enrichmentOutcome = !data && !error ? 'loading' : error || !data ? 'error' : 'success';
52
+ useContentEnrichmentDisplay({
53
+ capture,
54
+ enrichmentSource: 'bluesky_profile',
55
+ outcome: enrichmentOutcome,
56
+ cardKind: 'bluesky_profile',
57
+ engagementSurface,
58
+ properties: { handle },
59
+ });
60
+ // Skeleton
61
+ if (!data && !error) {
62
+ return (_jsxs("div", { className: "w-[420px] max-w-[420px] rounded-2xl overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-[#16202a] shadow-md", children: [_jsx("div", { className: "w-full h-[120px] bg-gradient-to-br from-[#0085ff] via-[#0060c4] to-[#6a00f4]" }), _jsxs("div", { className: "px-4 pb-4", children: [_jsx("div", { className: "mt-[-36px] mb-2.5 inline-block", children: _jsx("div", { className: "w-[72px] h-[72px] rounded-full border-[3px] border-white dark:border-[#16202a] bg-gray-200 dark:bg-gray-700 animate-pulse" }) }), _jsx("div", { className: "h-5 w-[55%] bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" }), _jsx("div", { className: "h-3.5 w-[35%] bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3.5" }), _jsx("div", { className: "h-3 w-[90%] bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-1.5" }), _jsx("div", { className: "h-3 w-[55%] bg-gray-200 dark:bg-gray-700 rounded animate-pulse" })] })] }));
63
+ }
64
+ // Error
65
+ if (error || !data) {
66
+ return (_jsx("div", { className: "w-[420px] max-w-[420px] rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-[#16202a] shadow-md", children: _jsxs("div", { className: "flex items-start gap-2.5 p-5 text-sm text-red-500 dark:text-red-400", children: [_jsx("span", { children: "\u26A0" }), _jsxs("span", { children: ["Failed to load @", handle, ": ", _jsx("em", { children: error?.message })] })] }) }));
67
+ }
68
+ const initials = (data.displayName || data.handle || '?')[0].toUpperCase();
69
+ return (_jsxs("div", { className: "w-[420px] max-w-[420px] rounded-2xl overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-[#16202a] text-[#0f1419] dark:text-[#e7e9ea] shadow-md", children: [_jsx("div", { className: "w-full h-[120px] overflow-hidden", children: data.banner ? (_jsx("img", { src: data.banner, alt: "", className: "block object-cover my-0 w-full h-full" })) : (_jsx("div", { className: "w-full h-full bg-gradient-to-br from-[#0085ff] via-[#0060c4] to-[#6a00f4]" })) }), _jsxs("div", { className: "px-4 pb-3.5", children: [_jsx("div", { className: "mt-[-36px] mb-2.5 inline-block", children: data.avatar ? (_jsx("img", { src: data.avatar, alt: data.displayName || data.handle, className: "w-[72px] h-[72px] rounded-full object-cover border-[3px] border-white dark:border-[#16202a] shadow my-0" })) : (_jsx("div", { className: "w-[72px] h-[72px] rounded-full bg-gradient-to-br from-[#0085ff] to-[#6a00f4] flex items-center justify-center text-white text-3xl font-bold border-[3px] border-white dark:border-[#16202a] shadow", children: initials })) }), _jsxs("div", { className: "mb-2", children: [_jsx("div", { className: "text-lg font-extrabold tracking-tight leading-tight", children: data.displayName || data.handle }), _jsxs("div", { className: "text-sm text-[#536471] dark:text-[#8b98a5] mt-0.5", children: ["@", data.handle] })] }), data.description && (_jsx("p", { className: "mb-3 text-sm leading-snug whitespace-pre-wrap break-words", children: data.description })), _jsx("div", { className: "flex flex-wrap gap-4 mb-3", children: [
70
+ [fmt(data.followersCount), 'Followers'],
71
+ [fmt(data.followsCount), 'Following'],
72
+ [fmt(data.postsCount), 'Posts'],
73
+ ].map(([num, label]) => (_jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { className: "text-sm font-bold", children: num }), _jsx("span", { className: "text-xs text-[#536471] dark:text-[#8b98a5]", children: label })] }, label))) }), _jsx("div", { className: "flex justify-end mt-1", children: _jsxs("a", { href: profileUrl, target: "_blank", rel: "noopener noreferrer", onClick: (e) => {
74
+ e.stopPropagation();
75
+ captureProfileLinkClick();
76
+ }, className: "text-xs text-[#0085ff] no-underline inline-flex items-center gap-1 opacity-70 hover:opacity-100 hover:underline transition-opacity", children: [_jsx(BskyIcon, {}), "View on Bluesky"] }) })] })] }));
77
+ }
78
+ // ─── Renderers ────────────────────────────────────────────────────────────────
79
+ export const BlueskyBlockRenderer = ({ node }) => {
80
+ const { handle } = (node.data ?? {});
81
+ if (!handle)
82
+ return null;
83
+ return _jsx(BlueskyProfileCard, { handle: handle, engagementSurface: "inline_block" });
84
+ };
85
+ export const BlueskyLinkRenderer = ({ node }) => {
86
+ const capture = useAnalytics();
87
+ const { handle, kind } = (node.data ?? {});
88
+ const captureTriggerClick = useCallback(() => {
89
+ if (!node.url)
90
+ return;
91
+ captureContentLinkClicked(capture, {
92
+ linkKind: kind === 'profile' ? 'bluesky_profile' : 'bluesky_post',
93
+ destinationUrl: node.url,
94
+ engagementSurface: 'trigger',
95
+ cardKind: kind === 'profile' ? 'bluesky_profile' : undefined,
96
+ properties: { handle, blueskyKind: kind },
97
+ });
98
+ }, [capture, handle, kind, node.url]);
99
+ // Only profile cards are implemented; fall back to a plain link for posts
100
+ // or any URL we couldn't fully parse.
101
+ if (!handle || kind !== 'profile') {
102
+ return (_jsx("a", { href: node.url, target: "_blank", rel: "noopener noreferrer", onClick: captureTriggerClick, children: _jsx(MyST, { ast: node.children }) }));
103
+ }
104
+ return (_jsx(HoverPopover, { card: _jsx(BlueskyProfileCard, { handle: handle, engagementSurface: "hover_card" }), analytics: { cardKind: 'bluesky_profile', properties: { handle } }, children: _jsx("a", { href: node.url, target: "_blank", rel: "noopener noreferrer", onClick: captureTriggerClick, children: _jsx(MyST, { ast: node.children }) }) }));
105
+ };
106
+ export const BLUESKY_LINK_RENDERERS = {
107
+ link: {
108
+ 'link[protocol=bluesky]': BlueskyLinkRenderer,
109
+ },
110
+ block: {
111
+ 'block[kind=bluesky]': BlueskyBlockRenderer,
112
+ },
113
+ };
@@ -1,5 +1,6 @@
1
- export declare function CiteFigureBar({ doi: doiString, className }: {
1
+ export declare function CiteFigureBar({ doi: doiString, citeLabel, className, }: {
2
2
  doi: string;
3
+ citeLabel: string;
3
4
  className?: string;
4
5
  }): import("react/jsx-runtime").JSX.Element | null;
5
6
  //# sourceMappingURL=cite-figurebar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cite-figurebar.d.ts","sourceRoot":"","sources":["../../src/components/cite-figurebar.tsx"],"names":[],"mappings":"AAwFA,wBAAgB,aAAa,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,kDA2D/F"}
1
+ {"version":3,"file":"cite-figurebar.d.ts","sourceRoot":"","sources":["../../src/components/cite-figurebar.tsx"],"names":[],"mappings":"AAkJA,wBAAgB,aAAa,CAAC,EAC5B,GAAG,EAAE,SAAS,EACd,SAAS,EACT,SAAS,GACV,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,kDA4EA"}
@@ -1,10 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import useSWR from 'swr';
3
4
  import { selectAll } from 'unist-util-select';
4
5
  import { Image, LightboxContainer } from '@curvenote/theme-ui';
5
6
  import { cn } from '@curvenote/react-utils';
6
7
  import { MyST } from 'myst-to-react';
7
8
  import { doi } from 'doi-utils';
9
+ import { useAnalytics } from '@curvenote/theme-ui';
10
+ import { useContentEnrichmentDisplay } from '../utils/content-analytics';
11
+ const OPENRXIV_READER = 'https://reader.openrxivlabs.org';
12
+ // DOI prefixes served by the openRxiv reader: bioRxiv/medRxiv (10.1101) and openRxiv (10.64898).
13
+ // These fetch full content from the reader; everything else falls back to the curvenote overlay.
14
+ const OPENRXIV_PREFIXES = new Set(['10.1101', '10.64898']);
15
+ // Picks the content endpoint for a cited DOI. Reader responses use relative `/img/...` urls, so
16
+ // we also return the base they resolve against; overlay responses already have absolute image urls.
17
+ function contentSource(doiString) {
18
+ const normalized = doi.normalize(doiString);
19
+ const prefix = normalized?.split('/')[0];
20
+ if (normalized && prefix && OPENRXIV_PREFIXES.has(prefix)) {
21
+ return { url: `${OPENRXIV_READER}/content/${normalized}.json`, imageBase: OPENRXIV_READER };
22
+ }
23
+ return { url: `https://overlay.curvenote.dev/doi/${normalized}.json` };
24
+ }
25
+ function resolveImageUrl(url, base) {
26
+ if (!url || !base || /^https?:\/\//i.test(url))
27
+ return url;
28
+ return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
29
+ }
8
30
  // Fetcher function for the OpenRxiv API
9
31
  async function openRxivFetcher(url) {
10
32
  const response = await fetch(url);
@@ -29,45 +51,69 @@ function useOpenRxivData(url) {
29
51
  };
30
52
  }
31
53
  // Specialized image component for figures
32
- function FigureImageComponent({ image, caption, }) {
33
- return (_jsx("div", { className: "overflow-hidden relative bg-gray-50 rounded-lg", children: _jsx(Image, { src: image.url, srcOptimized: image.urlOptimized, urlSource: image.urlSource, alt: image.alt || 'Figure', className: "object-cover w-full h-auto", caption: caption }) }));
54
+ function FigureImageComponent({ figure, citeLabel, citeDoi, onError, }) {
55
+ const capture = useAnalytics();
56
+ return (_jsx("div", { className: cn('overflow-hidden relative bg-gray-50 rounded-lg h-40',
57
+ // Force a constant height and contain the figure regardless of whether the
58
+ // Image wraps the <img> in a <picture> (when an optimized source exists).
59
+ '[&_picture]:block [&_picture]:w-full [&_picture]:h-full', '[&_img]:w-full [&_img]:h-full [&_img]:object-contain'), children: _jsx(Image, { src: figure.src, srcOptimized: figure.srcOptimized, urlSource: figure.urlSource, alt: figure.alt || 'Figure', caption: figure.caption, onError: onError, onLightboxOpen: () => capture('content_image_lightbox_opened', {
60
+ surface: 'content',
61
+ imageSource: 'cite_figure_bar',
62
+ citeLabel,
63
+ citeDoi,
64
+ imageUrl: figure.src,
65
+ imageAlt: figure.alt,
66
+ }) }) }));
34
67
  }
35
68
  // Main component that renders up to 3 figures
36
- export function CiteFigureBar({ doi: doiString, className }) {
37
- const url = `https://overlay.curvenote.dev/doi/${doi.normalize(doiString)}.json`;
69
+ export function CiteFigureBar({ doi: doiString, citeLabel, className, }) {
70
+ const { url, imageBase } = contentSource(doiString);
38
71
  const { data, error, isLoading } = useOpenRxivData(url);
39
- if (isLoading || error || !data?.mdast) {
40
- return null;
41
- }
42
- // Extract figure containers from the mdast
43
- const figures = selectAll('container[kind=figure]', data.mdast);
44
- // Take only the first 3 figures
45
- const displayFigures = figures.slice(0, 3);
46
- if (displayFigures.length === 0) {
47
- return null;
48
- }
49
- const imageList = figures
72
+ const capture = useAnalytics();
73
+ // `src` of images that failed to load — these figures are dropped from the bar.
74
+ const [failed, setFailed] = useState(new Set());
75
+ const allFigures = (data?.mdast ? selectAll('container[kind=figure]', data.mdast) : [])
50
76
  .map((figure) => {
51
77
  const image = figure.children.find((child) => child.type === 'image');
52
78
  const caption = figure.children.find((child) => child.type === 'caption');
53
79
  if (!image)
54
80
  return null;
55
81
  return {
56
- src: image.url,
57
- srcOptimized: image.urlOptimized,
82
+ key: figure.key,
83
+ src: resolveImageUrl(image.url, imageBase) ?? image.url,
84
+ srcOptimized: resolveImageUrl(image.urlOptimized, imageBase),
58
85
  urlSource: image.urlSource,
59
86
  alt: image.alt,
60
87
  caption: caption,
61
88
  };
62
89
  })
63
- .filter((image) => image !== null);
64
- return (_jsx(LightboxContainer, { imageList: imageList, children: _jsx("div", { className: cn('grid grid-cols-1 gap-4 md:grid-cols-3', className), children: displayFigures.map((figure, index) => {
65
- // Find the image within the figure container
66
- const image = figure.children.find((child) => child.type === 'image');
67
- const caption = figure.children.find((child) => child.type === 'caption');
68
- if (!image) {
69
- return null;
70
- }
71
- return (_jsxs("div", { className: "flex flex-col", children: [_jsx(FigureImageComponent, { image: image, caption: caption }), _jsx(MyST, { ast: caption, className: "text-xs line-clamp-5" })] }, figure.key || index));
72
- }) }) }));
90
+ .filter((figure) => figure !== null);
91
+ // Remove figures whose image failed to load, then show up to 3.
92
+ const visibleFigures = allFigures.filter((figure) => !failed.has(figure.src));
93
+ const displayFigures = visibleFigures.slice(0, 3);
94
+ const enrichmentOutcome = error ? 'error' : displayFigures.length > 0 ? 'success' : 'empty';
95
+ useContentEnrichmentDisplay({
96
+ capture,
97
+ enrichmentSource: 'openrxiv',
98
+ outcome: enrichmentOutcome,
99
+ cardKind: 'citation',
100
+ engagementSurface: 'hover_card',
101
+ enabled: !isLoading,
102
+ properties: { citeLabel, citeDoi: doiString, figureCount: displayFigures.length },
103
+ });
104
+ if (isLoading || error || !data?.mdast) {
105
+ return null;
106
+ }
107
+ if (displayFigures.length === 0) {
108
+ return null;
109
+ }
110
+ const markFailed = (src) => setFailed((prev) => (prev.has(src) ? prev : new Set(prev).add(src)));
111
+ const imageList = visibleFigures.map((figure) => ({
112
+ src: figure.src,
113
+ srcOptimized: figure.srcOptimized,
114
+ urlSource: figure.urlSource,
115
+ alt: figure.alt,
116
+ caption: figure.caption,
117
+ }));
118
+ return (_jsx(LightboxContainer, { imageList: imageList, children: _jsx("div", { className: cn('grid grid-cols-1 gap-4 md:grid-cols-3', className), children: displayFigures.map((figure, index) => (_jsxs("div", { className: "flex flex-col", children: [_jsx(FigureImageComponent, { figure: figure, citeLabel: citeLabel, citeDoi: doiString, onError: () => markFailed(figure.src) }), _jsx(MyST, { ast: figure.caption, className: "text-xs line-clamp-5" })] }, figure.key || index))) }) }));
73
119
  }
@@ -1,6 +1,17 @@
1
1
  import type { NodeRenderer } from '@myst-theme/providers';
2
2
  import type { GenericParent } from 'myst-common';
3
3
  export declare const CiteGroup: NodeRenderer<GenericParent>;
4
+ /**
5
+ * Conservatively recover a `https://doi.org/...` URL from a known publisher URL.
6
+ *
7
+ * Only URLs that map unambiguously to a DOI are handled. Anything we are not
8
+ * confident about returns `undefined` rather than guessing.
9
+ *
10
+ * @example
11
+ * doiUrlFromPublisherUrl('https://www.nature.com/articles/s41586-024-07441-w')
12
+ * // => 'https://doi.org/10.1038/s41586-024-07441-w'
13
+ */
14
+ export declare function doiUrlFromPublisherUrl(refUrl?: string): string | undefined;
4
15
  export declare const Cite: ({ label, error, children, className, }: {
5
16
  label?: string;
6
17
  error?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"cite.d.ts","sourceRoot":"","sources":["../../src/components/cite.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAoHjD,eAAO,MAAM,SAAS,EAAE,YAAY,CAAC,aAAa,CAiBjD,CAAC;AAEF,eAAO,MAAM,IAAI,GAAI,wCAKlB;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,4CAkCA,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,YAO1B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAGvD,CAAC"}
1
+ {"version":3,"file":"cite.d.ts","sourceRoot":"","sources":["../../src/components/cite.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAoLjD,eAAO,MAAM,SAAS,EAAE,YAAY,CAAC,aAAa,CAiBjD,CAAC;AAWF;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAW1E;AAoBD,eAAO,MAAM,IAAI,GAAI,wCAKlB;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,4CAkEA,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,YAO1B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAGvD,CAAC"}
@@ -1,23 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useReferences, useSiteManifest } from '@myst-theme/providers';
3
3
  import { doi } from 'doi-utils';
4
- import { HoverPopover, MyST } from 'myst-to-react';
4
+ import { MyST } from 'myst-to-react';
5
+ import { HoverPopover } from './hover-popover';
5
6
  import { createId } from 'myst-common';
6
7
  import { visit } from 'unist-util-visit';
7
8
  import { cn } from '@curvenote/react-utils';
8
9
  import { InlineError } from './inlineError';
9
10
  import { useOpenAlexWork } from '../hooks/useOpenAlex';
10
11
  import { reverseInvertedAbstract, formatAuthors } from '../utils/abstract';
11
- import { LinkCard } from '@curvenote/theme-ui';
12
+ import { LinkCard, useAnalytics } from '@curvenote/theme-ui';
12
13
  import { CiteFigureBar } from './cite-figurebar';
13
14
  import { fromHtml } from 'hast-util-from-html';
14
15
  import { toMdast } from 'hast-util-to-mdast';
15
16
  import { CiteYouTube, isYouTubeUrl } from './cite-youtube';
17
+ import { useCallback } from 'react';
18
+ import { captureContentCiteClicked, captureContentLinkClicked, useContentEnrichmentDisplay, } from '../utils/content-analytics';
16
19
  // TODO:
17
20
  // this should be turned into an upgrade step when we do citations.
18
21
  // We are only expecting very basic HTML in the citations, so this should be fine.
19
22
  function convertHtmlToMdast(html) {
20
23
  const hast = fromHtml(html, { fragment: true });
24
+ // The types play better hast-util-to-mdast@10, but we downgraded to 8 to align with myst.
21
25
  const mdast = toMdast(hast);
22
26
  // Ensure all nodes have a keys for react rendering errors
23
27
  visit(mdast, (node) => {
@@ -30,16 +34,56 @@ function useNumberedReferences() {
30
34
  const numbered_references = !!config?.options?.numbered_references;
31
35
  return numbered_references;
32
36
  }
33
- function CiteChild({ html, doi: doiString, url }) {
37
+ function CiteChild({ html, doi: doiString, url, citeLabel, }) {
38
+ const capture = useAnalytics();
34
39
  const { data: openAlexData, error, isLoading } = useOpenAlexWork(doiString);
35
- if (doiString && !error) {
40
+ const citeProperties = { citeLabel, doi: doiString, url };
41
+ const showOpenAlexCard = Boolean(doiString && doi.validate(doiString) && !error);
42
+ const openAlexOutcome = isLoading
43
+ ? 'loading'
44
+ : error
45
+ ? 'error'
46
+ : openAlexData
47
+ ? 'success'
48
+ : 'empty';
49
+ useContentEnrichmentDisplay({
50
+ capture,
51
+ enrichmentSource: 'openalex',
52
+ outcome: openAlexOutcome,
53
+ cardKind: 'citation',
54
+ engagementSurface: 'hover_card',
55
+ enabled: Boolean(doiString),
56
+ properties: {
57
+ citeLabel,
58
+ doi: doiString,
59
+ url,
60
+ hasAbstract: Boolean(openAlexData?.abstract_inverted_index),
61
+ hasTitle: Boolean(openAlexData?.title),
62
+ },
63
+ });
64
+ useContentEnrichmentDisplay({
65
+ capture,
66
+ enrichmentSource: 'cite_html',
67
+ outcome: 'fallback',
68
+ cardKind: 'citation',
69
+ engagementSurface: 'hover_card',
70
+ enabled: !showOpenAlexCard,
71
+ properties: { citeLabel, doi: doiString, url, hasYouTube: Boolean(url && isYouTubeUrl(url)) },
72
+ });
73
+ if (showOpenAlexCard && ['success', 'loading'].includes(openAlexOutcome)) {
36
74
  const abstract = openAlexData?.abstract_inverted_index
37
75
  ? reverseInvertedAbstract(openAlexData.abstract_inverted_index)
38
76
  : '';
39
77
  const authors = formatAuthors(openAlexData?.authorships);
40
78
  const journal = openAlexData?.primary_location?.source?.display_name;
41
79
  const year = openAlexData?.publication_year;
42
- return (_jsx(LinkCard, { url: doi.buildUrl(doiString), title: journal && year ? `${journal} (${year})` : journal || year, size: "lg", description: _jsxs("div", { className: "flex flex-col gap-3 max-w-none prose", children: [openAlexData?.title && (_jsx("div", { className: "flex flex-col gap-1 font-bold", children: _jsx("div", { className: "text-sm leading-relaxed", children: openAlexData.title.replace(/<[^>]+>/g, '') }) })), authors && (_jsx("div", { className: "flex flex-col gap-1", children: _jsx("div", { className: "text-sm", children: authors }) })), abstract && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-xs font-bold", children: "Abstract" }), _jsx("div", { className: "overflow-y-auto max-h-48 text-xs leading-relaxed", children: abstract })] })), _jsx(CiteFigureBar, { doi: doiString }), isLoading && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString }), _jsx("div", { className: "text-xs text-gray-500", children: "Loading additional information..." })] })), error && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString }), _jsx("div", { className: "text-xs text-gray-500", children: "Unable to load additional information" })] })), !isLoading && !error && !openAlexData && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString })] }))] }) }));
80
+ return (_jsx(LinkCard, { url: doi.buildUrl(doiString), title: journal && year ? `${journal} (${year})` : journal || year, size: "lg", onLinkClick: () => captureContentLinkClicked(capture, {
81
+ linkKind: 'citation',
82
+ destinationUrl: doi.buildUrl(doiString),
83
+ engagementSurface: 'hover_card',
84
+ cardKind: 'citation',
85
+ properties: citeProperties,
86
+ }), description: _jsxs("div", { className: "flex flex-col gap-3 max-w-none prose", children: [openAlexData?.title && (_jsx("div", { className: "flex flex-col gap-1 font-bold", children: _jsx("div", { className: "text-sm leading-relaxed", children: openAlexData.title.replace(/<[^>]+>/g, '') }) })), authors && (_jsx("div", { className: "flex flex-col gap-1", children: _jsx("div", { className: "text-sm", children: authors }) })), abstract && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-xs font-bold", children: "Abstract" }), _jsx("div", { className: "overflow-y-auto max-h-48 text-xs leading-relaxed", children: abstract })] })), _jsx(CiteFigureBar, { doi: doiString, citeLabel: citeLabel }), isLoading && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString }), _jsx("div", { className: "text-xs text-gray-500", children: "Loading additional information..." })] })), error && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString }), _jsx("div", { className: "text-xs text-gray-500", children: "Unable to load additional information" })] })), !isLoading && !error && !openAlexData && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString })] }))] }) }));
43
87
  }
44
88
  const mdast = convertHtmlToMdast(html);
45
89
  return (_jsxs("div", { className: "hover-document article w-[500px] sm:max-w-[500px] p-3", children: [url && isYouTubeUrl(url) && _jsx(CiteYouTube, { url: url }), _jsx(MyST, { ast: mdast })] }));
@@ -53,18 +97,96 @@ export const CiteGroup = ({ node, className }) => {
53
97
  parenthetical: node.kind === 'parenthetical',
54
98
  }, className), children: _jsx(MyST, { ast: node.children }) }));
55
99
  };
100
+ function isUrl(url) {
101
+ try {
102
+ new URL(url);
103
+ return true;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ /**
110
+ * Conservatively recover a `https://doi.org/...` URL from a known publisher URL.
111
+ *
112
+ * Only URLs that map unambiguously to a DOI are handled. Anything we are not
113
+ * confident about returns `undefined` rather than guessing.
114
+ *
115
+ * @example
116
+ * doiUrlFromPublisherUrl('https://www.nature.com/articles/s41586-024-07441-w')
117
+ * // => 'https://doi.org/10.1038/s41586-024-07441-w'
118
+ */
119
+ export function doiUrlFromPublisherUrl(refUrl) {
120
+ if (!refUrl || !isUrl(refUrl))
121
+ return undefined;
122
+ const parsed = new URL(refUrl);
123
+ const host = parsed.hostname.replace(/^www\./, '');
124
+ // Nature (Springer Nature) articles use the DOI prefix 10.1038 and embed the
125
+ // suffix directly in the path, e.g. /articles/s41586-024-07441-w
126
+ if (host === 'nature.com') {
127
+ const match = parsed.pathname.match(/^\/articles\/([a-z0-9.-]+)$/i);
128
+ if (match)
129
+ return `https://doi.org/10.1038/${match[1]}`;
130
+ }
131
+ return undefined;
132
+ }
133
+ function getDoi(doiString, refUrl) {
134
+ const cleanDoi = doi.validate(doiString);
135
+ if (cleanDoi)
136
+ return doi.buildUrl(doiString);
137
+ const url = getUrl(doiString, refUrl);
138
+ if (!url || !isUrl(url))
139
+ return undefined;
140
+ const publisherDoi = doiUrlFromPublisherUrl(url);
141
+ if (publisherDoi)
142
+ return publisherDoi;
143
+ return undefined;
144
+ }
145
+ function getUrl(doiString, refUrl) {
146
+ const cleanDoi = doi.validate(doiString);
147
+ if (cleanDoi)
148
+ return doi.buildUrl(doiString);
149
+ if (refUrl && isUrl(refUrl))
150
+ return refUrl;
151
+ if (doiString && isUrl(doiString))
152
+ return doiString;
153
+ return undefined;
154
+ }
56
155
  export const Cite = ({ label, error, children, className, }) => {
57
156
  const references = useReferences();
157
+ const capture = useAnalytics();
58
158
  if (!label) {
59
159
  return (_jsx(InlineError, { value: "cite (no label)", message: 'Citation Has No Label', className: className }));
60
160
  }
61
- const { html, doi: doiString, url: refUrl } = references?.cite?.data[label] ?? {};
161
+ const { html, doi: doiStringData, url: refUrlData } = references?.cite?.data[label] ?? {};
162
+ const doiString = getDoi(doiStringData, refUrlData);
163
+ const url = getUrl(doiStringData, refUrlData);
62
164
  if (error) {
63
165
  return _jsx(InlineError, { value: label, message: 'Citation Not Found', className: className });
64
166
  }
65
- const url = doiString ? doi.buildUrl(doiString) : refUrl;
66
167
  const isButtonLike = (className ?? '').split(' ').includes('button');
67
- return (_jsx(HoverPopover, { openDelay: 300, card: _jsx(CiteChild, { html: html, doi: doiString, url: url }), children: _jsxs("cite", { className: className, children: [url && (_jsx("a", { href: url, target: "_blank", rel: "noreferrer", className: cn({ 'hover-link': !isButtonLike }), children: children })), !url && _jsx("span", { className: "hover-link", children: children })] }) }));
168
+ const captureTriggerClick = useCallback(() => {
169
+ if (url) {
170
+ captureContentLinkClicked(capture, {
171
+ linkKind: 'citation',
172
+ destinationUrl: url,
173
+ engagementSurface: 'trigger',
174
+ cardKind: 'citation',
175
+ properties: { citeLabel: label, doi: doiString, url },
176
+ });
177
+ return;
178
+ }
179
+ captureContentCiteClicked(capture, {
180
+ citeLabel: label,
181
+ doi: doiString,
182
+ hasUrl: false,
183
+ engagementSurface: 'trigger',
184
+ });
185
+ }, [capture, url, label, doiString]);
186
+ return (_jsx(HoverPopover, { openDelay: 300, card: _jsx(CiteChild, { html: html, doi: doiString, url: url, citeLabel: label }), analytics: {
187
+ cardKind: 'citation',
188
+ properties: { citeLabel: label, doi: doiString, url },
189
+ }, children: _jsxs("cite", { className: className, children: [url && (_jsx("a", { href: url, target: "_blank", rel: "noreferrer", className: cn({ 'hover-link': !isButtonLike }), onClick: captureTriggerClick, children: children })), !url && (_jsx("span", { className: "hover-link", onClick: captureTriggerClick, children: children }))] }) }));
68
190
  };
69
191
  export const CiteRenderer = ({ node, className }) => {
70
192
  const numbered = useNumberedReferences();
@@ -0,0 +1,11 @@
1
+ export type CopyIconAnalytics = {
2
+ contentKind?: string;
3
+ properties?: Record<string, unknown>;
4
+ };
5
+ export type CopyIconProps = {
6
+ text: string;
7
+ className?: string;
8
+ analytics?: CopyIconAnalytics;
9
+ };
10
+ export declare function CopyIcon({ text, className, analytics }: CopyIconProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=copy-icon.d.ts.map