@raystack/chronicle 0.5.4 → 0.6.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 (68) hide show
  1. package/dist/cli/index.js +258 -80
  2. package/package.json +7 -5
  3. package/src/cli/commands/build.ts +5 -8
  4. package/src/cli/commands/dev.ts +5 -6
  5. package/src/cli/commands/init.test.ts +77 -0
  6. package/src/cli/commands/init.ts +73 -40
  7. package/src/cli/commands/serve.ts +6 -9
  8. package/src/cli/commands/start.ts +5 -5
  9. package/src/cli/utils/config.ts +6 -12
  10. package/src/cli/utils/scaffold.test.ts +179 -0
  11. package/src/cli/utils/scaffold.ts +70 -9
  12. package/src/components/api/field-row.tsx +1 -1
  13. package/src/components/api/field-section.tsx +2 -2
  14. package/src/components/mdx/index.tsx +1 -1
  15. package/src/components/ui/breadcrumbs.tsx +4 -2
  16. package/src/components/ui/client-theme-switcher.tsx +21 -4
  17. package/src/components/ui/search.module.css +16 -41
  18. package/src/components/ui/search.tsx +30 -41
  19. package/src/lib/config.test.ts +493 -0
  20. package/src/lib/config.ts +123 -22
  21. package/src/lib/head.tsx +23 -5
  22. package/src/lib/llms.test.ts +94 -0
  23. package/src/lib/llms.ts +41 -0
  24. package/src/lib/navigation.test.ts +94 -0
  25. package/src/lib/navigation.ts +51 -0
  26. package/src/lib/page-context.tsx +51 -32
  27. package/src/lib/route-resolver.test.ts +173 -0
  28. package/src/lib/route-resolver.ts +73 -0
  29. package/src/lib/source.ts +94 -1
  30. package/src/lib/version-source.test.ts +163 -0
  31. package/src/lib/version-source.ts +101 -0
  32. package/src/pages/ApiPage.tsx +1 -1
  33. package/src/pages/DocsLayout.tsx +24 -3
  34. package/src/pages/DocsPage.tsx +3 -6
  35. package/src/pages/LandingPage.module.css +56 -0
  36. package/src/pages/LandingPage.tsx +39 -0
  37. package/src/pages/NotFound.tsx +2 -0
  38. package/src/server/App.tsx +21 -23
  39. package/src/server/api/page.ts +5 -1
  40. package/src/server/api/search.ts +51 -24
  41. package/src/server/api/specs.ts +17 -5
  42. package/src/server/entry-client.tsx +42 -14
  43. package/src/server/entry-server.tsx +33 -11
  44. package/src/server/routes/[...slug].md.ts +0 -6
  45. package/src/server/routes/[version]/llms.txt.ts +26 -0
  46. package/src/server/routes/llms.txt.ts +10 -13
  47. package/src/server/routes/og.tsx +2 -2
  48. package/src/server/routes/sitemap.xml.ts +14 -6
  49. package/src/server/vite-config.ts +5 -5
  50. package/src/themes/default/ContentDirButtons.tsx +66 -0
  51. package/src/themes/default/Layout.module.css +187 -40
  52. package/src/themes/default/Layout.tsx +166 -65
  53. package/src/themes/default/OpenInAI.tsx +112 -0
  54. package/src/themes/default/Page.module.css +30 -0
  55. package/src/themes/default/Page.tsx +1 -3
  56. package/src/themes/default/SidebarLogo.tsx +26 -0
  57. package/src/themes/default/Toc.module.css +102 -25
  58. package/src/themes/default/Toc.tsx +56 -10
  59. package/src/themes/default/VersionSwitcher.tsx +59 -0
  60. package/src/themes/paper/ContentDirDropdown.tsx +47 -0
  61. package/src/themes/paper/Layout.module.css +7 -0
  62. package/src/themes/paper/Layout.tsx +20 -13
  63. package/src/themes/paper/VersionSwitcher.tsx +60 -0
  64. package/src/types/config.ts +145 -23
  65. package/src/types/content.ts +11 -1
  66. package/src/types/theme.ts +1 -0
  67. package/src/components/ui/footer.module.css +0 -27
  68. package/src/components/ui/footer.tsx +0 -30
@@ -1,79 +1,226 @@
1
1
  .layout {
2
+ --navbar-height: 48px;
2
3
  min-height: 100vh;
3
4
  }
4
5
 
5
- .header {
6
- border-bottom: 1px solid var(--rs-color-border-base-primary);
7
- }
8
-
9
- .search {
10
- margin-left: var(--rs-space-5);
11
- }
12
-
13
6
  .body {
14
7
  flex: 1;
15
8
  }
16
9
 
17
10
  .sidebar {
18
- width: 260px;
11
+ width: 262px;
12
+ flex: 0 0 262px;
13
+ display: flex;
14
+ flex-direction: column;
19
15
  position: sticky;
20
16
  top: 0;
21
17
  height: 100vh;
18
+ background: var(--rs-color-background-base-secondary);
22
19
  }
23
20
 
24
- .content {
25
- flex: 1;
26
- padding: var(--rs-space-9);
21
+ .sidebarLogo {
22
+ width: 28px;
23
+ height: 28px;
24
+ object-fit: contain;
27
25
  }
28
26
 
29
- .sidebarList {
30
- list-style: none;
27
+ .configIcon {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ width: 16px;
32
+ height: 16px;
33
+ color: currentColor;
34
+ }
35
+
36
+ .configIcon svg {
37
+ width: 100%;
38
+ height: 100%;
39
+ display: block;
40
+ }
41
+
42
+ .sidebar[data-position='left'] {
43
+ border-right: none;
31
44
  padding: 0;
32
- margin: 0;
33
45
  }
34
46
 
35
- .separator {
36
- height: 1px;
37
- background: var(--rs-color-border-base-primary);
38
- margin: var(--rs-space-3) 0;
47
+ .sidebarNavbar {
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ gap: var(--rs-space-3);
52
+ width: 100%;
53
+ height: var(--navbar-height);
54
+ padding: 0 var(--rs-space-5);
55
+ flex-shrink: 0;
56
+ backdrop-filter: blur(1px);
57
+ }
58
+
59
+ .sidebarNavLogo {
60
+ color: var(--rs-color-foreground-base-primary);
61
+ }
62
+
63
+ .sidebarNavActions {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--rs-space-3);
67
+ }
68
+
69
+ .sidebarMain {
70
+ padding: var(--rs-space-7) var(--rs-space-5);
71
+ gap: 0;
72
+ min-height: 0;
73
+ overflow-y: auto;
74
+ scrollbar-width: none;
75
+ }
76
+
77
+ .topLinks {
78
+ width: 100%;
79
+ margin-bottom: var(--rs-space-7);
80
+ }
81
+
82
+ .topLinkItem {
83
+ color: var(--rs-color-foreground-base-tertiary);
84
+ }
85
+
86
+ .topLinkText {
87
+ color: var(--rs-color-foreground-base-tertiary);
88
+ }
89
+
90
+ .topLinkItem[data-active='true'] {
91
+ background: transparent;
92
+ color: var(--rs-color-foreground-base-primary);
93
+ }
94
+
95
+ .topLinkItem[data-active='true'] .topLinkText {
96
+ color: var(--rs-color-foreground-base-primary);
97
+ }
98
+
99
+ .sidebarFooter {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: var(--rs-space-3);
103
+ height: var(--navbar-height);
104
+ box-sizing: border-box;
105
+ padding: 0 var(--rs-space-5);
106
+ border-top: 0.5px solid var(--rs-color-border-base-primary);
107
+ background: var(--rs-color-background-base-secondary);
108
+ backdrop-filter: blur(7.5px);
109
+ }
110
+
111
+ .sidebarMain::-webkit-scrollbar {
112
+ display: none;
39
113
  }
40
114
 
41
- .folder {
42
- margin-bottom: var(--rs-space-3);
115
+ .sidebarHeader {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: space-between;
119
+ gap: var(--rs-space-3);
120
+ height: var(--navbar-height);
121
+ box-sizing: border-box;
122
+ margin-bottom: 0;
123
+ padding: 0 var(--rs-space-5);
124
+ background: var(--rs-color-background-base-secondary);
125
+ border-bottom: 0.5px solid var(--rs-color-border-base-primary);
126
+ border-radius: 0;
127
+ backdrop-filter: blur(1px);
128
+ }
129
+
130
+ .mainArea {
131
+ flex: 1;
132
+ min-width: 0;
43
133
  }
44
134
 
45
- .folderLabel {
46
- font-weight: 500;
47
- font-size: 0.875rem;
48
- color: var(--rs-color-text-base-secondary);
49
- text-transform: uppercase;
50
- letter-spacing: 0.05em;
135
+ .cardWrapper {
136
+ flex: 1;
137
+ display: flex;
138
+ padding: 0 0 var(--rs-space-2) 0;
139
+ min-height: 0;
140
+ background: var(--rs-color-background-base-secondary);
141
+ }
142
+
143
+ .card {
144
+ flex: 1;
145
+ display: flex;
146
+ flex-direction: column;
147
+ border-left: 0.5px solid var(--rs-color-border-base-primary);
148
+ box-shadow: var(--rs-shadow-soft);
149
+ overflow: visible;
150
+ }
151
+
152
+ .subNav {
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: space-between;
156
+ height: var(--navbar-height);
157
+ padding: var(--rs-space-4) var(--rs-space-7);
158
+ background: var(--rs-color-background-base-primary);
159
+ border-bottom: 0.5px solid var(--rs-color-border-base-primary);
160
+ backdrop-filter: blur(1px);
161
+ }
162
+
163
+ .subNavLeft {
164
+ min-width: 0;
165
+ }
166
+
167
+ .content {
168
+ flex: 1;
169
+ padding: var(--rs-space-9) var(--rs-space-7);
170
+ background: var(--rs-color-background-base-primary);
51
171
  }
52
172
 
53
- .folder > .sidebarList {
54
- margin-top: var(--rs-space-2);
173
+ .groupItems {
55
174
  padding-left: var(--rs-space-4);
175
+ padding-bottom: var(--rs-space-3);
176
+ gap: 0;
177
+ }
178
+
179
+ .navGroup {
180
+ margin-top: 0;
56
181
  }
57
182
 
58
- .navButton {
183
+ .navGroup[data-depth='0'] {
184
+ margin-top: var(--rs-space-7);
185
+ }
186
+
187
+ .navGroup .navGroupHeader {
188
+ margin: 0;
189
+ }
190
+
191
+ .navGroup[data-depth='1'] .navGroupHeader {
192
+ height: auto;
193
+ }
194
+
195
+ .navGroup[data-depth='1'] .navGroupLabel {
196
+ color: var(--rs-color-foreground-base-primary);
197
+ }
198
+
199
+ .navGroupTrigger {
59
200
  display: flex;
201
+ padding: var(--rs-space-3);
60
202
  align-items: center;
203
+ gap: var(--rs-space-3);
204
+ align-self: stretch;
205
+ width: 100%;
61
206
  height: 32px;
62
- padding: 0 var(--rs-space-4);
63
- border: 1px solid var(--rs-color-border-base-primary);
64
207
  border-radius: var(--rs-radius-2);
65
- font-size: var(--rs-font-size-small);
66
- font-weight: var(--rs-font-weight-medium);
67
- color: var(--rs-color-foreground-base-primary);
68
- text-decoration: none;
208
+ box-sizing: border-box;
69
209
  }
70
210
 
71
- .navButton:hover {
72
- background: var(--rs-color-background-base-primary-hover);
211
+ .navGroupLabel {
212
+ flex: 1;
213
+ text-align: left;
214
+ color: var(--rs-color-foreground-base-secondary);
215
+ font-family: var(--rs-font-body);
216
+ font-size: var(--rs-font-size-small);
217
+ font-weight: var(--rs-font-weight-medium);
218
+ line-height: var(--rs-line-height-small);
219
+ letter-spacing: var(--rs-letter-spacing-small);
73
220
  }
74
221
 
75
- .groupItems {
76
- padding-left: var(--rs-space-4);
222
+ .navGroupChevron {
223
+ margin-left: auto;
77
224
  }
78
225
 
79
226
  .page {
@@ -1,22 +1,28 @@
1
- import { RectangleStackIcon } from '@heroicons/react/24/outline';
2
1
  import {
3
- Button,
4
- Flex,
5
- Headline,
6
- Link,
7
- Navbar,
8
- Sidebar
9
- } from '@raystack/apsara';
2
+ ArrowLeftIcon,
3
+ ArrowRightIcon,
4
+ CodeBracketSquareIcon,
5
+ RectangleStackIcon,
6
+ DocumentTextIcon,
7
+ Squares2X2Icon
8
+ } from '@heroicons/react/24/outline';
9
+ import { Flex, IconButton, Sidebar } from '@raystack/apsara';
10
10
  import { cx } from 'class-variance-authority';
11
- import { useEffect, useRef } from 'react';
12
- import { Link as RouterLink, useLocation } from 'react-router';
11
+ import { useEffect, useMemo, useRef } from 'react';
12
+ import { Link as RouterLink, useLocation, useNavigate } from 'react-router';
13
13
  import { MethodBadge } from '@/components/api/method-badge';
14
14
  import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
15
- import { Footer } from '@/components/ui/footer';
16
15
  import { Search } from '@/components/ui/search';
16
+ import { Breadcrumbs } from '@/components/ui/breadcrumbs';
17
+ import { getLandingEntries } from '@/lib/config';
18
+ import { getActiveContentDir } from '@/lib/navigation';
19
+ import { usePageContext } from '@/lib/page-context';
17
20
  import type { Node } from 'fumadocs-core/page-tree';
18
21
  import type { ThemeLayoutProps } from '@/types';
19
22
  import styles from './Layout.module.css';
23
+ import { OpenInAI } from './OpenInAI';
24
+ import { SidebarLogo } from './SidebarLogo';
25
+ import { VersionSwitcher } from './VersionSwitcher';
20
26
 
21
27
  const iconMap: Record<string, React.ReactNode> = {
22
28
  'rectangle-stack': <RectangleStackIcon width={16} height={16} />,
@@ -27,16 +33,51 @@ const iconMap: Record<string, React.ReactNode> = {
27
33
  'method-patch': <MethodBadge method='PATCH' size='micro' />
28
34
  };
29
35
 
36
+ function renderConfigIcon(
37
+ icon: string | undefined,
38
+ alt: string,
39
+ fallback: React.ReactNode
40
+ ): React.ReactNode {
41
+ if (!icon) return fallback;
42
+ if (icon.trim().startsWith('<svg')) {
43
+ return (
44
+ <span
45
+ aria-label={alt}
46
+ className={styles.configIcon}
47
+ dangerouslySetInnerHTML={{ __html: icon }}
48
+ />
49
+ );
50
+ }
51
+ return <img src={icon} alt={alt} className={styles.configIcon} />;
52
+ }
53
+
30
54
  let savedScrollTop = 0;
31
55
 
32
56
  export function Layout({
33
57
  children,
34
58
  config,
35
59
  tree,
60
+ hideSidebar,
36
61
  classNames
37
62
  }: ThemeLayoutProps) {
38
63
  const { pathname } = useLocation();
64
+ const navigate = useNavigate();
65
+ const { page, version } = usePageContext();
39
66
  const scrollRef = useRef<HTMLDivElement>(null);
67
+ const isApiRoute = pathname === '/apis' || pathname.startsWith('/apis/');
68
+ const isApiBase = (basePath: string) =>
69
+ pathname === basePath || pathname.startsWith(`${basePath}/`);
70
+ const { prev, next } = page ?? { prev: null, next: null };
71
+
72
+ const contentEntries = getLandingEntries(config, version.dir);
73
+ const activeContentDir = getActiveContentDir(pathname, config);
74
+ const apiEntries = config.api ?? [];
75
+ const showTopLinks = contentEntries.length + apiEntries.length > 1;
76
+
77
+ const slug = useMemo(
78
+ () => (pathname === '/' ? [] : pathname.split('/').filter(Boolean)),
79
+ [pathname]
80
+ );
40
81
 
41
82
  useEffect(() => {
42
83
  const el = scrollRef.current;
@@ -58,87 +99,147 @@ export function Layout({
58
99
 
59
100
  return (
60
101
  <Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
61
- <Navbar className={styles.header}>
62
- <Navbar.Start>
63
- <RouterLink
64
- to='/'
65
- style={{ textDecoration: 'none', color: 'inherit' }}
66
- >
67
- <Headline size='small' weight='medium' as='h1'>
68
- {config.title}
69
- </Headline>
70
- </RouterLink>
71
- </Navbar.Start>
72
- <Navbar.End>
73
- <Flex gap='medium' align='center' className={styles.navActions}>
74
- {config.api?.map(api => (
75
- <RouterLink
76
- key={api.basePath}
77
- to={api.basePath}
78
- className={styles.navButton}
79
- >
80
- {api.name} API
81
- </RouterLink>
82
- ))}
83
- {config.navigation?.links?.map(link => (
84
- <Link key={link.href} href={link.href}>
85
- {link.label}
86
- </Link>
87
- ))}
88
- {config.search?.enabled && <Search />}
89
- </Flex>
90
- <ClientThemeSwitcher size={16} />
91
- </Navbar.End>
92
- </Navbar>
93
102
  <Flex className={cx(styles.body, classNames?.body)}>
94
- <Sidebar
95
- defaultOpen
96
- collapsible={false}
97
- className={cx(styles.sidebar, classNames?.sidebar)}
98
- >
99
- <Sidebar.Main ref={scrollRef}>
100
- {tree.children.map((item, i) => (
101
- <SidebarNode
102
- key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
103
- item={item}
104
- pathname={pathname}
105
- />
106
- ))}
107
- </Sidebar.Main>
108
- </Sidebar>
109
- <main className={cx(styles.content, classNames?.content)}>
110
- {children}
111
- </main>
103
+ {hideSidebar ? null : (
104
+ <Sidebar
105
+ defaultOpen
106
+ collapsible={false}
107
+ className={cx(styles.sidebar, classNames?.sidebar)}
108
+ >
109
+ <Sidebar.Header className={styles.sidebarHeader}>
110
+ <SidebarLogo config={config} />
111
+ <Flex gap='small' align='center'>
112
+ {config.search?.enabled && <Search />}
113
+ <ClientThemeSwitcher size={16} />
114
+ </Flex>
115
+ </Sidebar.Header>
116
+ <Sidebar.Main ref={scrollRef} className={styles.sidebarMain}>
117
+ {showTopLinks ? (
118
+ <div className={styles.topLinks}>
119
+ {contentEntries.map(entry => (
120
+ <Sidebar.Item
121
+ key={entry.href}
122
+ href={entry.href}
123
+ active={activeContentDir === entry.contentDir}
124
+ leadingIcon={renderConfigIcon(
125
+ entry.icon,
126
+ entry.label,
127
+ <DocumentTextIcon width={16} height={16} />
128
+ )}
129
+ classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
130
+ render={<RouterLink to={entry.href} />}
131
+ >
132
+ {entry.label}
133
+ </Sidebar.Item>
134
+ ))}
135
+ {apiEntries.map(api => (
136
+ <Sidebar.Item
137
+ key={api.basePath}
138
+ href={api.basePath}
139
+ active={isApiBase(api.basePath)}
140
+ leadingIcon={renderConfigIcon(
141
+ api.icon,
142
+ api.name,
143
+ <CodeBracketSquareIcon width={16} height={16} />
144
+ )}
145
+ classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
146
+ render={<RouterLink to={api.basePath} />}
147
+ >
148
+ {api.name} API
149
+ </Sidebar.Item>
150
+ ))}
151
+ </div>
152
+ ) : null}
153
+ {tree.children.map((item, i) => (
154
+ <SidebarNode
155
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
156
+ item={item}
157
+ pathname={pathname}
158
+ />
159
+ ))}
160
+ </Sidebar.Main>
161
+ {config.versions?.length ? (
162
+ <Sidebar.Footer className={styles.sidebarFooter}>
163
+ <VersionSwitcher />
164
+ </Sidebar.Footer>
165
+ ) : null}
166
+ </Sidebar>
167
+ )}
168
+ <Flex direction='column' className={styles.mainArea}>
169
+ <div className={styles.cardWrapper}>
170
+ <div className={styles.card}>
171
+ <nav className={styles.subNav}>
172
+ <Flex align='center' gap='small' className={styles.subNavLeft}>
173
+ <Flex align='center' gap='extra-small'>
174
+ <IconButton
175
+ size={2}
176
+ disabled={!prev}
177
+ onClick={() => prev && navigate(prev.url)}
178
+ aria-label='Previous page'
179
+ >
180
+ <ArrowLeftIcon width={14} height={14} />
181
+ </IconButton>
182
+ <IconButton
183
+ size={2}
184
+ disabled={!next}
185
+ onClick={() => next && navigate(next.url)}
186
+ aria-label='Next page'
187
+ >
188
+ <ArrowRightIcon width={14} height={14} />
189
+ </IconButton>
190
+ </Flex>
191
+ {!isApiRoute && <Breadcrumbs slug={slug} tree={tree} />}
192
+ </Flex>
193
+ <OpenInAI />
194
+ </nav>
195
+ <main className={cx(styles.content, classNames?.content)}>
196
+ {children}
197
+ </main>
198
+ </div>
199
+ </div>
200
+ </Flex>
112
201
  </Flex>
113
- <Footer config={config.footer} />
114
202
  </Flex>
115
203
  );
116
204
  }
117
205
 
118
206
  function SidebarNode({
119
207
  item,
120
- pathname
208
+ pathname,
209
+ depth = 0
121
210
  }: {
122
211
  item: Node;
123
212
  pathname: string;
213
+ depth?: number;
124
214
  }) {
125
215
  if (item.type === 'separator') {
126
216
  return null;
127
217
  }
128
218
 
129
219
  if (item.type === 'folder') {
220
+ if (depth > 1) return null;
130
221
  const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
131
222
  return (
132
223
  <Sidebar.Group
224
+ className={styles.navGroup}
225
+ data-depth={depth}
133
226
  label={item.name?.toString() ?? ''}
134
227
  leadingIcon={icon ?? undefined}
135
- classNames={{ items: styles.groupItems }}
228
+ collapsible={depth === 1}
229
+ classNames={{
230
+ items: styles.groupItems,
231
+ header: styles.navGroupHeader,
232
+ trigger: styles.navGroupTrigger,
233
+ label: styles.navGroupLabel,
234
+ chevron: styles.navGroupChevron,
235
+ }}
136
236
  >
137
237
  {item.children.map((child, i) => (
138
238
  <SidebarNode
139
239
  key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
140
240
  item={child}
141
241
  pathname={pathname}
242
+ depth={depth + 1}
142
243
  />
143
244
  ))}
144
245
  </Sidebar.Group>
@@ -155,7 +256,7 @@ function SidebarNode({
155
256
  href={href}
156
257
  active={isActive}
157
258
  leadingIcon={icon ?? undefined}
158
- as={link}
259
+ render={link}
159
260
  >
160
261
  {item.name}
161
262
  </Sidebar.Item>
@@ -0,0 +1,112 @@
1
+ 'use client';
2
+
3
+ import {
4
+ ChevronDownIcon,
5
+ ClipboardDocumentIcon,
6
+ DocumentTextIcon,
7
+ SparklesIcon
8
+ } from '@heroicons/react/24/outline';
9
+ import { Button, Menu } from '@raystack/apsara';
10
+ import { useCallback } from 'react';
11
+
12
+ function ClaudeIcon() {
13
+ return (
14
+ <svg
15
+ xmlns='http://www.w3.org/2000/svg'
16
+ width='14'
17
+ height='14'
18
+ viewBox='0 0 24 24'
19
+ fill='currentColor'
20
+ fillRule='evenodd'
21
+ aria-hidden='true'
22
+ >
23
+ <title>Claude</title>
24
+ <path d='M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z' />
25
+ </svg>
26
+ );
27
+ }
28
+
29
+ function ChatGPTIcon() {
30
+ return (
31
+ <svg
32
+ xmlns='http://www.w3.org/2000/svg'
33
+ width='14'
34
+ height='14'
35
+ viewBox='0 0 12 12'
36
+ fill='none'
37
+ aria-hidden='true'
38
+ >
39
+ <path
40
+ d='M4.60254 4.37898V3.24915C4.60254 3.154 4.63825 3.08261 4.72146 3.0351L6.99307 1.72688C7.30228 1.54849 7.67098 1.46528 8.0515 1.46528C9.47862 1.46528 10.3825 2.57135 10.3825 3.74869C10.3825 3.83193 10.3825 3.92708 10.3706 4.02224L8.01581 2.64263C7.87311 2.55942 7.73034 2.55942 7.58765 2.64263L4.60254 4.37898ZM9.90677 8.77939V6.07964C9.90677 5.91311 9.83537 5.79418 9.6927 5.71095L6.70759 3.9746L7.6828 3.41559C7.76603 3.36808 7.83742 3.36808 7.92065 3.41559L10.1922 4.72381C10.8464 5.10443 11.2864 5.91311 11.2864 6.69799C11.2864 7.60182 10.7512 8.43436 9.90677 8.77929V8.77939ZM3.90086 6.40083L2.92565 5.83C2.84244 5.78248 2.80672 5.71107 2.80672 5.61592V2.9995C2.80672 1.727 3.78194 0.763609 5.10208 0.763609C5.60165 0.763609 6.06537 0.930148 6.45794 1.22746L4.11504 2.58329C3.97237 2.6665 3.90099 2.78543 3.90099 2.95199V6.40092L3.90086 6.40083ZM5.99999 7.61387L4.60254 6.82896V5.16401L5.99999 4.37911L7.39734 5.16401V6.82896L5.99999 7.61387ZM6.89789 11.2294C6.39835 11.2294 5.93463 11.0628 5.54206 10.7655L7.88493 9.40968C8.02763 9.32647 8.09901 9.20755 8.09901 9.04098V5.59205L9.08618 6.16288C9.16938 6.21039 9.2051 6.28178 9.2051 6.37696V8.99337C9.2051 10.2659 8.21794 11.2293 6.89789 11.2293V11.2294ZM4.07925 8.57726L1.80764 7.26906C1.15348 6.88842 0.713498 6.07977 0.713498 5.29486C0.713498 4.37911 1.26058 3.55851 2.10492 3.21358V5.92515C2.10492 6.09169 2.17633 6.21062 2.319 6.29385L5.29229 8.01825L4.31707 8.57726C4.23387 8.62477 4.16246 8.62477 4.07925 8.57726ZM3.9485 10.5277C2.60459 10.5277 1.61745 9.51678 1.61745 8.26802C1.61745 8.17287 1.62938 8.07772 1.6412 7.98256L3.9841 9.33839C4.12677 9.42163 4.26956 9.42163 4.41223 9.33839L7.39734 7.61399V8.74382C7.39734 8.83898 7.36165 8.91036 7.27841 8.95788L5.00683 10.2661C4.69759 10.4445 4.3289 10.5277 3.94838 10.5277H3.9485ZM6.89789 11.9429C8.33696 11.9429 9.53808 10.9201 9.81172 9.5643C11.1437 9.21937 12 7.97061 12 6.69811C12 5.86557 11.6433 5.05691 11.001 4.47414C11.0605 4.22437 11.0962 3.9746 11.0962 3.72495C11.0962 2.02429 9.71656 0.751662 8.12288 0.751662C7.80185 0.751662 7.49262 0.799178 7.18338 0.906279C6.64812 0.382967 5.91076 0.0499878 5.10208 0.0499878C3.66304 0.0499878 2.46192 1.07272 2.18828 2.42855C0.856291 2.77348 0 4.02224 0 5.29474C0 6.12728 0.356749 6.93594 0.998986 7.51871C0.939523 7.76848 0.903831 8.01825 0.903831 8.26793C0.903831 9.96859 2.28344 11.2412 3.87709 11.2412C4.19815 11.2412 4.50738 11.1937 4.81662 11.0866C5.35175 11.6099 6.08912 11.9429 6.89789 11.9429Z'
41
+ fill='currentColor'
42
+ />
43
+ </svg>
44
+ );
45
+ }
46
+
47
+ function mdUrl() {
48
+ return `${window.location.origin}${window.location.pathname}.md`;
49
+ }
50
+
51
+ async function copyMd() {
52
+ try {
53
+ const res = await fetch(mdUrl());
54
+ const text = await res.text();
55
+ await navigator.clipboard.writeText(text);
56
+ } catch {
57
+ // ignore
58
+ }
59
+ }
60
+
61
+ export function OpenInAI() {
62
+ const onCopy = useCallback(() => {
63
+ void copyMd();
64
+ }, []);
65
+ const onView = useCallback(() => {
66
+ window.open(mdUrl(), '_blank', 'noopener,noreferrer');
67
+ }, []);
68
+ const onChatGPT = useCallback(() => {
69
+ const q = encodeURIComponent(`Read ${mdUrl()}`);
70
+ window.open(`https://chatgpt.com/?q=${q}`, '_blank', 'noopener,noreferrer');
71
+ }, []);
72
+ const onClaude = useCallback(() => {
73
+ const q = encodeURIComponent(`Read ${mdUrl()}`);
74
+ window.open(`https://claude.ai/new?q=${q}`, '_blank', 'noopener,noreferrer');
75
+ }, []);
76
+
77
+ return (
78
+ <Menu>
79
+ <Menu.Trigger
80
+ render={
81
+ <Button
82
+ size='small'
83
+ variant='outline'
84
+ color='neutral'
85
+ leadingIcon={<SparklesIcon width={12} height={12} />}
86
+ trailingIcon={<ChevronDownIcon width={12} height={12} />}
87
+ />
88
+ }
89
+ >
90
+ Open in AI
91
+ </Menu.Trigger>
92
+ <Menu.Content>
93
+ <Menu.Item onClick={onCopy}>
94
+ <ClipboardDocumentIcon width={14} height={14} />
95
+ Copy as MD
96
+ </Menu.Item>
97
+ <Menu.Item onClick={onView}>
98
+ <DocumentTextIcon width={14} height={14} />
99
+ View MD
100
+ </Menu.Item>
101
+ <Menu.Item onClick={onChatGPT}>
102
+ <ChatGPTIcon />
103
+ Open in ChatGPT
104
+ </Menu.Item>
105
+ <Menu.Item onClick={onClaude}>
106
+ <ClaudeIcon />
107
+ Open in Claude
108
+ </Menu.Item>
109
+ </Menu.Content>
110
+ </Menu>
111
+ );
112
+ }