blodemd 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dev-server/app/[[...slug]]/page.tsx +139 -0
- package/dev-server/app/blodemd-dev/invalidate/route.ts +12 -0
- package/dev-server/app/blodemd-dev/static/[...path]/route.ts +32 -0
- package/dev-server/app/blodemd-dev/version/route.ts +14 -0
- package/dev-server/app/blodemd-internal/proxy/route.ts +86 -0
- package/dev-server/app/error.tsx +24 -0
- package/dev-server/app/globals.css +4 -0
- package/dev-server/app/layout.tsx +38 -0
- package/dev-server/app/not-found.tsx +18 -0
- package/dev-server/app/search/route.ts +17 -0
- package/dev-server/components/dev-reload-script.tsx +86 -0
- package/dev-server/components/providers.tsx +15 -0
- package/dev-server/lib/dev-state.ts +8 -0
- package/dev-server/lib/local-content-source.ts +103 -0
- package/dev-server/lib/local-runtime.tsx +558 -0
- package/dev-server/next.config.js +46 -0
- package/dev-server/package.json +57 -0
- package/dev-server/postcss.config.mjs +7 -0
- package/dev-server/public/glide-variable.woff2 +0 -0
- package/dev-server/tsconfig.json +49 -0
- package/dist/cli.mjs +108 -39
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +455 -0
- package/docs/components/api/api-playground.tsx +295 -0
- package/docs/components/api/api-reference.tsx +121 -0
- package/docs/components/content/collection-index.tsx +114 -0
- package/docs/components/docs/contextual-menu.tsx +406 -0
- package/docs/components/docs/copy-page-menu.tsx +255 -0
- package/docs/components/docs/doc-header.tsx +192 -0
- package/docs/components/docs/doc-shell.tsx +289 -0
- package/docs/components/docs/doc-sidebar.tsx +206 -0
- package/docs/components/docs/doc-toc.tsx +45 -0
- package/docs/components/docs/mobile-nav.tsx +207 -0
- package/docs/components/mdx/accordion.tsx +83 -0
- package/docs/components/mdx/badge.tsx +79 -0
- package/docs/components/mdx/callout.tsx +88 -0
- package/docs/components/mdx/card.tsx +104 -0
- package/docs/components/mdx/code-block.tsx +75 -0
- package/docs/components/mdx/code-group.tsx +94 -0
- package/docs/components/mdx/color.tsx +87 -0
- package/docs/components/mdx/columns.tsx +25 -0
- package/docs/components/mdx/expandable.tsx +45 -0
- package/docs/components/mdx/field-layout.tsx +77 -0
- package/docs/components/mdx/frame.tsx +23 -0
- package/docs/components/mdx/get-text-content.ts +18 -0
- package/docs/components/mdx/icon.tsx +56 -0
- package/docs/components/mdx/index.tsx +96 -0
- package/docs/components/mdx/installer.tsx +20 -0
- package/docs/components/mdx/panel.tsx +11 -0
- package/docs/components/mdx/param-field.tsx +56 -0
- package/docs/components/mdx/preview.tsx +36 -0
- package/docs/components/mdx/prompt.tsx +63 -0
- package/docs/components/mdx/request-example.tsx +27 -0
- package/docs/components/mdx/response-field.tsx +42 -0
- package/docs/components/mdx/steps.tsx +92 -0
- package/docs/components/mdx/tabs.tsx +88 -0
- package/docs/components/mdx/tile.tsx +43 -0
- package/docs/components/mdx/tooltip.tsx +71 -0
- package/docs/components/mdx/tree.tsx +120 -0
- package/docs/components/mdx/type-table.tsx +71 -0
- package/docs/components/mdx/update.tsx +44 -0
- package/docs/components/mdx/video.tsx +12 -0
- package/docs/components/mdx/view.tsx +66 -0
- package/docs/components/providers.tsx +15 -0
- package/docs/components/ui/breadcrumb.tsx +92 -0
- package/docs/components/ui/button.tsx +90 -0
- package/docs/components/ui/card.tsx +92 -0
- package/docs/components/ui/command.tsx +139 -0
- package/docs/components/ui/dialog.tsx +97 -0
- package/docs/components/ui/field.tsx +237 -0
- package/docs/components/ui/input.tsx +105 -0
- package/docs/components/ui/label.tsx +22 -0
- package/docs/components/ui/popover.tsx +72 -0
- package/docs/components/ui/search.tsx +380 -0
- package/docs/components/ui/separator.tsx +26 -0
- package/docs/components/ui/sheet.tsx +104 -0
- package/docs/components/ui/sidebar.tsx +433 -0
- package/docs/components/ui/theme-toggle.tsx +62 -0
- package/docs/components/ui/tooltip.tsx +53 -0
- package/docs/lib/contextual-options.ts +193 -0
- package/docs/lib/docs-collection.ts +22 -0
- package/docs/lib/mdx.ts +90 -0
- package/docs/lib/navigation.ts +288 -0
- package/docs/lib/openapi.ts +158 -0
- package/docs/lib/routes.ts +10 -0
- package/docs/lib/server-cache.ts +83 -0
- package/docs/lib/shiki.ts +35 -0
- package/docs/lib/theme.ts +29 -0
- package/docs/lib/toc.ts +2 -0
- package/docs/lib/utils.ts +5 -0
- package/package.json +33 -4
- package/packages/@repo/common/dist/index.d.ts +9 -0
- package/packages/@repo/common/dist/index.d.ts.map +1 -0
- package/packages/@repo/common/dist/index.js +42 -0
- package/packages/@repo/common/package.json +34 -0
- package/packages/@repo/common/src/common.unit.test.ts +55 -0
- package/packages/@repo/common/src/index.ts +51 -0
- package/packages/@repo/contracts/dist/api-key.d.ts +30 -0
- package/packages/@repo/contracts/dist/api-key.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/api-key.js +20 -0
- package/packages/@repo/contracts/dist/dates.d.ts +4 -0
- package/packages/@repo/contracts/dist/dates.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/dates.js +2 -0
- package/packages/@repo/contracts/dist/deployment.d.ts +71 -0
- package/packages/@repo/contracts/dist/deployment.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/deployment.js +46 -0
- package/packages/@repo/contracts/dist/domain.d.ts +94 -0
- package/packages/@repo/contracts/dist/domain.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/domain.js +36 -0
- package/packages/@repo/contracts/dist/ids.d.ts +14 -0
- package/packages/@repo/contracts/dist/ids.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/ids.js +10 -0
- package/packages/@repo/contracts/dist/index.d.ts +10 -0
- package/packages/@repo/contracts/dist/index.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/index.js +11 -0
- package/packages/@repo/contracts/dist/pagination.d.ts +23 -0
- package/packages/@repo/contracts/dist/pagination.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/pagination.js +15 -0
- package/packages/@repo/contracts/dist/project.d.ts +25 -0
- package/packages/@repo/contracts/dist/project.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/project.js +23 -0
- package/packages/@repo/contracts/dist/tenant.d.ts +99 -0
- package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/tenant.js +36 -0
- package/packages/@repo/contracts/dist/user.d.ts +9 -0
- package/packages/@repo/contracts/dist/user.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/user.js +9 -0
- package/packages/@repo/contracts/package.json +37 -0
- package/packages/@repo/contracts/src/api-key.ts +27 -0
- package/packages/@repo/contracts/src/dates.ts +4 -0
- package/packages/@repo/contracts/src/deployment.ts +73 -0
- package/packages/@repo/contracts/src/domain.ts +51 -0
- package/packages/@repo/contracts/src/ids.ts +22 -0
- package/packages/@repo/contracts/src/index.ts +11 -0
- package/packages/@repo/contracts/src/pagination.ts +21 -0
- package/packages/@repo/contracts/src/project.ts +30 -0
- package/packages/@repo/contracts/src/tenant.ts +54 -0
- package/packages/@repo/contracts/src/user.ts +12 -0
- package/packages/@repo/models/dist/docs-config.d.ts +985 -0
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -0
- package/packages/@repo/models/dist/docs-config.js +548 -0
- package/packages/@repo/models/dist/index.d.ts +3 -0
- package/packages/@repo/models/dist/index.d.ts.map +1 -0
- package/packages/@repo/models/dist/index.js +3 -0
- package/packages/@repo/models/dist/tenant.d.ts +25 -0
- package/packages/@repo/models/dist/tenant.d.ts.map +1 -0
- package/packages/@repo/models/dist/tenant.js +1 -0
- package/packages/@repo/models/package.json +37 -0
- package/packages/@repo/models/src/docs-config.ts +648 -0
- package/packages/@repo/models/src/index.ts +3 -0
- package/packages/@repo/models/src/tenant.ts +29 -0
- package/packages/@repo/prebuild/dist/index.d.ts +2 -0
- package/packages/@repo/prebuild/dist/index.d.ts.map +1 -0
- package/packages/@repo/prebuild/dist/index.js +2 -0
- package/packages/@repo/prebuild/dist/openapi.d.ts +43 -0
- package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -0
- package/packages/@repo/prebuild/dist/openapi.js +58 -0
- package/packages/@repo/prebuild/package.json +39 -0
- package/packages/@repo/prebuild/src/index.ts +2 -0
- package/packages/@repo/prebuild/src/openapi.ts +116 -0
- package/packages/@repo/previewing/dist/blob-source.d.ts +16 -0
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/blob-source.js +110 -0
- package/packages/@repo/previewing/dist/content-source.d.ts +12 -0
- package/packages/@repo/previewing/dist/content-source.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/content-source.js +1 -0
- package/packages/@repo/previewing/dist/fs-source.d.ts +11 -0
- package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/fs-source.js +79 -0
- package/packages/@repo/previewing/dist/index.d.ts +120 -0
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/index.js +984 -0
- package/packages/@repo/previewing/package.json +41 -0
- package/packages/@repo/previewing/src/blob-source.ts +167 -0
- package/packages/@repo/previewing/src/content-source.ts +12 -0
- package/packages/@repo/previewing/src/fs-source.ts +111 -0
- package/packages/@repo/previewing/src/index.ts +1490 -0
- package/packages/@repo/previewing/src/index.unit.test.ts +290 -0
- package/packages/@repo/validation/dist/index.d.ts +12 -0
- package/packages/@repo/validation/dist/index.d.ts.map +1 -0
- package/packages/@repo/validation/dist/index.js +30 -0
- package/packages/@repo/validation/package.json +37 -0
- package/packages/@repo/validation/src/index.ts +59 -0
- package/packages/@repo/validation/src/mintlify-docs-schema.json +5016 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ContextualOption } from "@repo/models";
|
|
4
|
+
import {
|
|
5
|
+
Checkmark1Icon,
|
|
6
|
+
ChevronDownSmallIcon,
|
|
7
|
+
ClaudeaiIcon,
|
|
8
|
+
CodeAssistantIcon,
|
|
9
|
+
CodeBracketsIcon,
|
|
10
|
+
CodeIcon,
|
|
11
|
+
CodeLinesIcon,
|
|
12
|
+
CopySimpleIcon,
|
|
13
|
+
GoogleColoredIcon,
|
|
14
|
+
GrokIcon,
|
|
15
|
+
MarkdownIcon,
|
|
16
|
+
OpenaiIcon,
|
|
17
|
+
PerplexityIcon,
|
|
18
|
+
Plugin1Icon,
|
|
19
|
+
SparkleIcon,
|
|
20
|
+
WindIcon,
|
|
21
|
+
} from "blode-icons-react";
|
|
22
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
23
|
+
import type { ComponentType, ReactNode, SVGProps } from "react";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
buildBuiltinUrl,
|
|
27
|
+
builtinOptions,
|
|
28
|
+
resolveCustomHref,
|
|
29
|
+
} from "@/lib/contextual-options";
|
|
30
|
+
|
|
31
|
+
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
|
|
32
|
+
|
|
33
|
+
const iconMap: Record<string, IconComponent> = {
|
|
34
|
+
ClaudeaiIcon,
|
|
35
|
+
CodeAssistantIcon,
|
|
36
|
+
CodeBracketsIcon,
|
|
37
|
+
CodeIcon,
|
|
38
|
+
CodeLinesIcon,
|
|
39
|
+
CopySimpleIcon,
|
|
40
|
+
GoogleColoredIcon,
|
|
41
|
+
GrokIcon,
|
|
42
|
+
MarkdownIcon,
|
|
43
|
+
OpenaiIcon,
|
|
44
|
+
PerplexityIcon,
|
|
45
|
+
Plugin1Icon,
|
|
46
|
+
SparkleIcon,
|
|
47
|
+
WindIcon,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getBuiltinIcon = (iconName: string): IconComponent =>
|
|
51
|
+
iconMap[iconName] ?? CopySimpleIcon;
|
|
52
|
+
|
|
53
|
+
type ActionId = "copy" | "mcp" | "add-mcp" | "assistant";
|
|
54
|
+
|
|
55
|
+
interface ResolvedOption {
|
|
56
|
+
key: string;
|
|
57
|
+
title: string;
|
|
58
|
+
description: string;
|
|
59
|
+
icon: IconComponent;
|
|
60
|
+
type: "action" | "link";
|
|
61
|
+
action?: ActionId;
|
|
62
|
+
href?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ContextualContext {
|
|
66
|
+
pageUrl: string;
|
|
67
|
+
pageContent: string;
|
|
68
|
+
pagePath: string;
|
|
69
|
+
mcpServerUrl?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolveOptions = (
|
|
73
|
+
options: ContextualOption[],
|
|
74
|
+
context: ContextualContext
|
|
75
|
+
): ResolvedOption[] => {
|
|
76
|
+
const resolved: ResolvedOption[] = [];
|
|
77
|
+
for (const option of options) {
|
|
78
|
+
if (typeof option === "string") {
|
|
79
|
+
const definition = builtinOptions[option];
|
|
80
|
+
if (!definition) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (definition.type === "action") {
|
|
84
|
+
resolved.push({
|
|
85
|
+
action: option as ActionId,
|
|
86
|
+
description: definition.description,
|
|
87
|
+
icon: getBuiltinIcon(definition.iconName),
|
|
88
|
+
key: option,
|
|
89
|
+
title: definition.title,
|
|
90
|
+
type: "action",
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
const href = buildBuiltinUrl(option, context);
|
|
94
|
+
if (href) {
|
|
95
|
+
resolved.push({
|
|
96
|
+
description: definition.description,
|
|
97
|
+
href,
|
|
98
|
+
icon: getBuiltinIcon(definition.iconName),
|
|
99
|
+
key: option,
|
|
100
|
+
title: definition.title,
|
|
101
|
+
type: "link",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
resolved.push({
|
|
107
|
+
description: option.description,
|
|
108
|
+
href: resolveCustomHref(option.href, context),
|
|
109
|
+
icon: CopySimpleIcon,
|
|
110
|
+
key: `custom-${option.title}`,
|
|
111
|
+
title: option.title,
|
|
112
|
+
type: "link",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return resolved;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const useContextualActions = (content: string, title: string) => {
|
|
120
|
+
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
121
|
+
|
|
122
|
+
const handleAction = useCallback(
|
|
123
|
+
async (action: string, key?: string) => {
|
|
124
|
+
const id = key ?? action;
|
|
125
|
+
switch (action) {
|
|
126
|
+
case "copy": {
|
|
127
|
+
await navigator.clipboard.writeText(`# ${title}\n\n${content}`);
|
|
128
|
+
setCopiedId(id);
|
|
129
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
default: {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
[content, title]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return { copiedId, handleAction };
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const usePageContext = (
|
|
144
|
+
content: string,
|
|
145
|
+
title: string,
|
|
146
|
+
pagePath: string
|
|
147
|
+
): ContextualContext => {
|
|
148
|
+
const [pageUrl, setPageUrl] = useState("");
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
setPageUrl(window.location.href);
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
mcpServerUrl: undefined,
|
|
156
|
+
pageContent: `# ${title}\n\n${content}`,
|
|
157
|
+
pagePath,
|
|
158
|
+
pageUrl,
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const MenuIcon = ({ children }: { children: ReactNode }) => (
|
|
163
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border">
|
|
164
|
+
{children}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const MenuItem = ({
|
|
169
|
+
children,
|
|
170
|
+
href,
|
|
171
|
+
onSelect,
|
|
172
|
+
}: {
|
|
173
|
+
children: ReactNode;
|
|
174
|
+
href?: string;
|
|
175
|
+
onSelect?: () => void;
|
|
176
|
+
}) =>
|
|
177
|
+
href ? (
|
|
178
|
+
<a
|
|
179
|
+
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors hover:bg-secondary/25"
|
|
180
|
+
href={href}
|
|
181
|
+
onClick={onSelect}
|
|
182
|
+
rel="noopener noreferrer"
|
|
183
|
+
target="_blank"
|
|
184
|
+
>
|
|
185
|
+
{children}
|
|
186
|
+
</a>
|
|
187
|
+
) : (
|
|
188
|
+
<button
|
|
189
|
+
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-secondary/25"
|
|
190
|
+
onClick={onSelect}
|
|
191
|
+
type="button"
|
|
192
|
+
>
|
|
193
|
+
{children}
|
|
194
|
+
</button>
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const ExternalArrow = () => (
|
|
198
|
+
<svg
|
|
199
|
+
aria-hidden="true"
|
|
200
|
+
className="ml-1 inline-block size-3"
|
|
201
|
+
fill="none"
|
|
202
|
+
stroke="currentColor"
|
|
203
|
+
strokeLinecap="round"
|
|
204
|
+
strokeLinejoin="round"
|
|
205
|
+
strokeWidth={2}
|
|
206
|
+
viewBox="0 0 24 24"
|
|
207
|
+
>
|
|
208
|
+
<path d="M7 17L17 7M7 7h10v10" />
|
|
209
|
+
</svg>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
interface ContextualMenuProps {
|
|
213
|
+
options: ContextualOption[];
|
|
214
|
+
content: string;
|
|
215
|
+
title: string;
|
|
216
|
+
pagePath: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const ContextualMenu = ({
|
|
220
|
+
options,
|
|
221
|
+
content,
|
|
222
|
+
title,
|
|
223
|
+
pagePath,
|
|
224
|
+
}: ContextualMenuProps) => {
|
|
225
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
226
|
+
const context = usePageContext(content, title, pagePath);
|
|
227
|
+
const { copiedId, handleAction } = useContextualActions(content, title);
|
|
228
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
229
|
+
|
|
230
|
+
const resolved = useMemo(
|
|
231
|
+
() => resolveOptions(options, context),
|
|
232
|
+
[context, options]
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const [primaryOption] = resolved;
|
|
236
|
+
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (!menuOpen) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const handlePointerDown = (event: MouseEvent) => {
|
|
243
|
+
if (menuRef.current?.contains(event.target as Node)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
setMenuOpen(false);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
250
|
+
return () => document.removeEventListener("mousedown", handlePointerDown);
|
|
251
|
+
}, [menuOpen]);
|
|
252
|
+
|
|
253
|
+
const closeMenu = useCallback(() => {
|
|
254
|
+
setMenuOpen(false);
|
|
255
|
+
}, []);
|
|
256
|
+
|
|
257
|
+
const toggleMenu = useCallback(() => {
|
|
258
|
+
setMenuOpen((current) => !current);
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
const handlePrimaryAction = useCallback(async () => {
|
|
262
|
+
if (!primaryOption) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (primaryOption.type === "link" && primaryOption.href) {
|
|
267
|
+
window.open(primaryOption.href, "_blank", "noopener,noreferrer");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await handleAction(primaryOption.action ?? "", primaryOption.key);
|
|
272
|
+
}, [handleAction, primaryOption]);
|
|
273
|
+
|
|
274
|
+
const actionHandlers = useMemo(
|
|
275
|
+
() =>
|
|
276
|
+
Object.fromEntries(
|
|
277
|
+
resolved
|
|
278
|
+
.filter((item) => item.type === "action")
|
|
279
|
+
.map((item) => [
|
|
280
|
+
item.key,
|
|
281
|
+
async () => {
|
|
282
|
+
await handleAction(item.action ?? "", item.key);
|
|
283
|
+
closeMenu();
|
|
284
|
+
},
|
|
285
|
+
])
|
|
286
|
+
),
|
|
287
|
+
[closeMenu, handleAction, resolved]
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (!primaryOption) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const isCopied = copiedId === primaryOption.key;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="relative flex shrink-0 items-center" ref={menuRef}>
|
|
298
|
+
<button
|
|
299
|
+
className="inline-flex items-center gap-2 rounded-l-xl border border-r-0 border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-secondary/25"
|
|
300
|
+
onClick={handlePrimaryAction}
|
|
301
|
+
type="button"
|
|
302
|
+
>
|
|
303
|
+
{primaryOption.type === "action" && isCopied ? (
|
|
304
|
+
<Checkmark1Icon aria-hidden="true" className="size-[18px]" />
|
|
305
|
+
) : (
|
|
306
|
+
<primaryOption.icon aria-hidden="true" className="size-[18px]" />
|
|
307
|
+
)}
|
|
308
|
+
<span>
|
|
309
|
+
{primaryOption.type === "action" && isCopied
|
|
310
|
+
? "Copied"
|
|
311
|
+
: primaryOption.title}
|
|
312
|
+
</span>
|
|
313
|
+
</button>
|
|
314
|
+
|
|
315
|
+
{resolved.length > 1 ? (
|
|
316
|
+
<>
|
|
317
|
+
<button
|
|
318
|
+
aria-expanded={menuOpen}
|
|
319
|
+
aria-label="More actions"
|
|
320
|
+
className="inline-flex items-center self-stretch rounded-r-xl border border-border px-2 transition-colors hover:bg-secondary/25"
|
|
321
|
+
onClick={toggleMenu}
|
|
322
|
+
type="button"
|
|
323
|
+
>
|
|
324
|
+
<ChevronDownSmallIcon
|
|
325
|
+
aria-hidden="true"
|
|
326
|
+
className="size-[18px] text-muted-foreground"
|
|
327
|
+
/>
|
|
328
|
+
</button>
|
|
329
|
+
{menuOpen ? (
|
|
330
|
+
<div className="absolute right-0 top-[calc(100%+0.25rem)] z-50 min-w-[320px] rounded-xl border border-border bg-background p-1 shadow-lg">
|
|
331
|
+
{resolved.slice(1).map((item) => (
|
|
332
|
+
<MenuItem
|
|
333
|
+
href={item.type === "link" ? item.href : undefined}
|
|
334
|
+
key={item.key}
|
|
335
|
+
onSelect={
|
|
336
|
+
item.type === "action"
|
|
337
|
+
? actionHandlers[item.key]
|
|
338
|
+
: closeMenu
|
|
339
|
+
}
|
|
340
|
+
>
|
|
341
|
+
<MenuIcon>
|
|
342
|
+
{item.type === "action" && copiedId === item.key ? (
|
|
343
|
+
<Checkmark1Icon
|
|
344
|
+
aria-hidden="true"
|
|
345
|
+
className="size-[18px]"
|
|
346
|
+
/>
|
|
347
|
+
) : (
|
|
348
|
+
<item.icon aria-hidden="true" className="size-[18px]" />
|
|
349
|
+
)}
|
|
350
|
+
</MenuIcon>
|
|
351
|
+
<div className="flex-1">
|
|
352
|
+
<div className="font-medium">
|
|
353
|
+
{item.title}
|
|
354
|
+
{item.type === "link" ? <ExternalArrow /> : null}
|
|
355
|
+
</div>
|
|
356
|
+
<div className="text-xs text-muted-foreground">
|
|
357
|
+
{item.description}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
</MenuItem>
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
363
|
+
) : null}
|
|
364
|
+
</>
|
|
365
|
+
) : null}
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export const ContextualTocItems = ({
|
|
371
|
+
content,
|
|
372
|
+
options,
|
|
373
|
+
title,
|
|
374
|
+
pagePath,
|
|
375
|
+
}: ContextualMenuProps) => {
|
|
376
|
+
const context = usePageContext(content, title, pagePath);
|
|
377
|
+
const resolved = useMemo(
|
|
378
|
+
() =>
|
|
379
|
+
resolveOptions(options, context).filter(
|
|
380
|
+
(item): item is ResolvedOption & { href: string; type: "link" } =>
|
|
381
|
+
item.type === "link" && Boolean(item.href)
|
|
382
|
+
),
|
|
383
|
+
[context, options]
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (!resolved.length) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<div className="grid gap-1.5">
|
|
392
|
+
{resolved.map((item) => (
|
|
393
|
+
<a
|
|
394
|
+
className="inline-flex items-center gap-2 rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
395
|
+
href={item.href}
|
|
396
|
+
key={item.key}
|
|
397
|
+
rel="noopener noreferrer"
|
|
398
|
+
target="_blank"
|
|
399
|
+
>
|
|
400
|
+
<item.icon aria-hidden="true" className="size-3.5 shrink-0" />
|
|
401
|
+
<span className="truncate">{item.title}</span>
|
|
402
|
+
</a>
|
|
403
|
+
))}
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { slugify } from "@repo/common";
|
|
4
|
+
import {
|
|
5
|
+
Checkmark1Icon,
|
|
6
|
+
ChevronDownSmallIcon,
|
|
7
|
+
ClaudeaiIcon,
|
|
8
|
+
CopySimpleIcon,
|
|
9
|
+
OpenaiIcon,
|
|
10
|
+
} from "blode-icons-react";
|
|
11
|
+
import type React from "react";
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
|
+
|
|
14
|
+
interface CopyPageMenuProps {
|
|
15
|
+
content?: string;
|
|
16
|
+
contentUrl?: string;
|
|
17
|
+
title: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LEADING_H1_REGEX = /^#\s+([^\r\n]+)(?:\r?\n(?:\r?\n)?)?/;
|
|
21
|
+
|
|
22
|
+
const stripMatchingLeadingH1 = (source: string, title: string) => {
|
|
23
|
+
const trimmed = source.trimStart();
|
|
24
|
+
const match = LEADING_H1_REGEX.exec(trimmed);
|
|
25
|
+
if (!match) {
|
|
26
|
+
return trimmed.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const [headingLine = "", headingTitle = ""] = match;
|
|
30
|
+
if (slugify(headingTitle) !== slugify(title)) {
|
|
31
|
+
return trimmed.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return trimmed.slice(headingLine.length).trim();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const formatMarkdownForCopy = (source: string, title: string) => {
|
|
38
|
+
const content = stripMatchingLeadingH1(source, title);
|
|
39
|
+
if (!content) {
|
|
40
|
+
return `# ${title}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `# ${title}\n\n${content}`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const MenuItem = ({
|
|
47
|
+
children,
|
|
48
|
+
href,
|
|
49
|
+
onSelect,
|
|
50
|
+
}: {
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
href?: string;
|
|
53
|
+
onSelect?: () => void;
|
|
54
|
+
}) =>
|
|
55
|
+
href ? (
|
|
56
|
+
<a
|
|
57
|
+
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors hover:bg-secondary/25"
|
|
58
|
+
href={href}
|
|
59
|
+
onClick={onSelect}
|
|
60
|
+
rel="noopener noreferrer"
|
|
61
|
+
target="_blank"
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</a>
|
|
65
|
+
) : (
|
|
66
|
+
<button
|
|
67
|
+
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors hover:bg-secondary/25"
|
|
68
|
+
onClick={onSelect}
|
|
69
|
+
type="button"
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const MenuIcon = ({ children }: { children: React.ReactNode }) => (
|
|
76
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border">
|
|
77
|
+
{children}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const ExternalArrow = () => (
|
|
82
|
+
<svg
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
className="ml-1 inline-block size-3"
|
|
85
|
+
fill="none"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
strokeLinecap="round"
|
|
88
|
+
strokeLinejoin="round"
|
|
89
|
+
strokeWidth={2}
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
>
|
|
92
|
+
<path d="M7 17L17 7M7 7h10v10" />
|
|
93
|
+
</svg>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
export const CopyPageMenu = ({
|
|
97
|
+
content,
|
|
98
|
+
contentUrl,
|
|
99
|
+
title,
|
|
100
|
+
}: CopyPageMenuProps) => {
|
|
101
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
102
|
+
const [copied, setCopied] = useState(false);
|
|
103
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
104
|
+
const [pageUrl, setPageUrl] = useState("");
|
|
105
|
+
const [resolvedContent, setResolvedContent] = useState(content ?? "");
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
setPageUrl(window.location.href);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (content !== undefined) {
|
|
113
|
+
setResolvedContent(content);
|
|
114
|
+
}
|
|
115
|
+
}, [content]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!menuOpen) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handlePointerDown = (event: MouseEvent) => {
|
|
123
|
+
if (menuRef.current?.contains(event.target as Node)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
setMenuOpen(false);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
130
|
+
return () => document.removeEventListener("mousedown", handlePointerDown);
|
|
131
|
+
}, [menuOpen]);
|
|
132
|
+
|
|
133
|
+
const closeMenu = useCallback(() => {
|
|
134
|
+
setMenuOpen(false);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
const toggleMenu = useCallback(() => {
|
|
138
|
+
setMenuOpen((current) => !current);
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
const getContent = useCallback(async () => {
|
|
142
|
+
if (resolvedContent) {
|
|
143
|
+
return resolvedContent;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!contentUrl) {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const response = await fetch(contentUrl, {
|
|
151
|
+
headers: {
|
|
152
|
+
accept: "text/markdown,text/plain;q=0.9,*/*;q=0.8",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error(`Failed to load page markdown: ${response.status}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const nextContent = await response.text();
|
|
160
|
+
setResolvedContent(nextContent);
|
|
161
|
+
return nextContent;
|
|
162
|
+
}, [contentUrl, resolvedContent]);
|
|
163
|
+
|
|
164
|
+
const handleCopy = useCallback(async () => {
|
|
165
|
+
const nextContent = await getContent();
|
|
166
|
+
const markdown = formatMarkdownForCopy(nextContent, title);
|
|
167
|
+
await navigator.clipboard.writeText(markdown);
|
|
168
|
+
setCopied(true);
|
|
169
|
+
closeMenu();
|
|
170
|
+
setTimeout(() => setCopied(false), 2000);
|
|
171
|
+
}, [closeMenu, getContent, title]);
|
|
172
|
+
|
|
173
|
+
const chatgptUrl = pageUrl
|
|
174
|
+
? `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it.`)}`
|
|
175
|
+
: "#";
|
|
176
|
+
const claudeUrl = pageUrl
|
|
177
|
+
? `https://claude.ai/new?q=${encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it.`)}`
|
|
178
|
+
: "#";
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className="relative flex shrink-0 items-center" ref={menuRef}>
|
|
182
|
+
<button
|
|
183
|
+
className="inline-flex items-center gap-2 rounded-l-xl border border-r-0 border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-secondary/25"
|
|
184
|
+
onClick={handleCopy}
|
|
185
|
+
type="button"
|
|
186
|
+
>
|
|
187
|
+
{copied ? (
|
|
188
|
+
<Checkmark1Icon aria-hidden="true" className="size-[18px]" />
|
|
189
|
+
) : (
|
|
190
|
+
<CopySimpleIcon aria-hidden="true" className="size-[18px]" />
|
|
191
|
+
)}
|
|
192
|
+
<span>{copied ? "Copied" : "Copy page"}</span>
|
|
193
|
+
</button>
|
|
194
|
+
|
|
195
|
+
<button
|
|
196
|
+
aria-expanded={menuOpen}
|
|
197
|
+
aria-label="More actions"
|
|
198
|
+
className="inline-flex items-center self-stretch rounded-r-xl border border-border px-2 transition-colors hover:bg-secondary/25"
|
|
199
|
+
onClick={toggleMenu}
|
|
200
|
+
type="button"
|
|
201
|
+
>
|
|
202
|
+
<ChevronDownSmallIcon
|
|
203
|
+
aria-hidden="true"
|
|
204
|
+
className="size-[18px] text-muted-foreground"
|
|
205
|
+
/>
|
|
206
|
+
</button>
|
|
207
|
+
|
|
208
|
+
{menuOpen ? (
|
|
209
|
+
<div className="absolute right-0 top-[calc(100%+0.25rem)] z-50 min-w-[280px] rounded-xl border border-border bg-background p-1 shadow-lg">
|
|
210
|
+
<MenuItem onSelect={handleCopy}>
|
|
211
|
+
<MenuIcon>
|
|
212
|
+
<CopySimpleIcon aria-hidden="true" className="size-[18px]" />
|
|
213
|
+
</MenuIcon>
|
|
214
|
+
<div>
|
|
215
|
+
<div className="font-medium">Copy page</div>
|
|
216
|
+
<div className="text-xs text-muted-foreground">
|
|
217
|
+
Copy page as Markdown for LLMs
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</MenuItem>
|
|
221
|
+
|
|
222
|
+
<MenuItem href={chatgptUrl} onSelect={closeMenu}>
|
|
223
|
+
<MenuIcon>
|
|
224
|
+
<OpenaiIcon aria-hidden="true" className="size-[18px]" />
|
|
225
|
+
</MenuIcon>
|
|
226
|
+
<div className="flex-1">
|
|
227
|
+
<div className="font-medium">
|
|
228
|
+
Open in ChatGPT
|
|
229
|
+
<ExternalArrow />
|
|
230
|
+
</div>
|
|
231
|
+
<div className="text-xs text-muted-foreground">
|
|
232
|
+
Ask questions about this page
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</MenuItem>
|
|
236
|
+
|
|
237
|
+
<MenuItem href={claudeUrl} onSelect={closeMenu}>
|
|
238
|
+
<MenuIcon>
|
|
239
|
+
<ClaudeaiIcon aria-hidden="true" className="size-[18px]" />
|
|
240
|
+
</MenuIcon>
|
|
241
|
+
<div className="flex-1">
|
|
242
|
+
<div className="font-medium">
|
|
243
|
+
Open in Claude
|
|
244
|
+
<ExternalArrow />
|
|
245
|
+
</div>
|
|
246
|
+
<div className="text-xs text-muted-foreground">
|
|
247
|
+
Ask questions about this page
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</MenuItem>
|
|
251
|
+
</div>
|
|
252
|
+
) : null}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
};
|