blodemd 0.0.7 → 0.0.9

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 (61) hide show
  1. package/README.md +25 -9
  2. package/dev-server/app/[[...slug]]/page.tsx +1 -0
  3. package/dev-server/next.config.js +11 -13
  4. package/dev-server/package.json +1 -1
  5. package/dev-server/tsconfig.json +3 -0
  6. package/dist/cli.mjs +869 -184
  7. package/dist/cli.mjs.map +1 -1
  8. package/docs/components/api/api-playground.tsx +255 -80
  9. package/docs/components/api/api-reference.tsx +11 -1
  10. package/docs/components/docs/contextual-menu.tsx +227 -142
  11. package/docs/components/docs/copy-page-menu.tsx +132 -85
  12. package/docs/components/docs/doc-header.tsx +13 -3
  13. package/docs/components/docs/doc-shell.tsx +22 -11
  14. package/docs/components/docs/mobile-nav.tsx +0 -6
  15. package/docs/components/mdx/code-group.tsx +171 -62
  16. package/docs/components/mdx/tabs.tsx +131 -26
  17. package/docs/components/ui/input.tsx +0 -1
  18. package/docs/components/ui/search.tsx +241 -132
  19. package/docs/lib/content-root.ts +33 -0
  20. package/docs/lib/content-source.ts +70 -0
  21. package/docs/lib/contextual-options.ts +20 -0
  22. package/docs/lib/docs-runtime.tsx +595 -0
  23. package/docs/lib/edge-config.ts +95 -0
  24. package/docs/lib/env.ts +22 -0
  25. package/docs/lib/openapi-proxy.ts +88 -0
  26. package/docs/lib/platform-config.ts +6 -0
  27. package/docs/lib/routes.ts +39 -0
  28. package/docs/lib/supabase.ts +13 -0
  29. package/docs/lib/tenancy.ts +322 -0
  30. package/docs/lib/tenant-headers.ts +14 -0
  31. package/docs/lib/tenant-static.ts +529 -0
  32. package/docs/lib/tenant-utility-context.ts +62 -0
  33. package/docs/lib/tenants.ts +68 -0
  34. package/docs/lib/use-mobile.ts +19 -0
  35. package/package.json +3 -2
  36. package/packages/@repo/common/dist/index.d.ts +7 -0
  37. package/packages/@repo/common/dist/index.d.ts.map +1 -1
  38. package/packages/@repo/common/dist/index.js +42 -0
  39. package/packages/@repo/common/src/index.ts +50 -0
  40. package/packages/@repo/contracts/dist/project.d.ts +1 -1
  41. package/packages/@repo/contracts/dist/project.js +1 -1
  42. package/packages/@repo/contracts/src/project.ts +1 -1
  43. package/packages/@repo/models/dist/docs-config.d.ts +194 -29
  44. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  45. package/packages/@repo/models/dist/docs-config.js +3 -28
  46. package/packages/@repo/models/src/docs-config.ts +5 -31
  47. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  48. package/packages/@repo/previewing/dist/blob-source.js +7 -2
  49. package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
  50. package/packages/@repo/previewing/dist/fs-source.js +2 -3
  51. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  52. package/packages/@repo/previewing/dist/index.js +1 -41
  53. package/packages/@repo/previewing/src/blob-source.ts +7 -4
  54. package/packages/@repo/previewing/src/fs-source.ts +2 -3
  55. package/packages/@repo/previewing/src/index.ts +3 -55
  56. package/packages/@repo/validation/dist/index.d.ts +2 -2
  57. package/packages/@repo/validation/dist/index.d.ts.map +1 -1
  58. package/packages/@repo/validation/dist/index.js +2 -2
  59. package/packages/@repo/validation/package.json +1 -0
  60. package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
  61. package/packages/@repo/validation/src/index.ts +4 -4
@@ -11,12 +11,20 @@ import {
11
11
  import type React from "react";
12
12
  import { useCallback, useEffect, useRef, useState } from "react";
13
13
 
14
+ import {
15
+ Popover,
16
+ PopoverContent,
17
+ PopoverTrigger,
18
+ } from "@/components/ui/popover";
19
+
14
20
  interface CopyPageMenuProps {
15
21
  content?: string;
16
22
  contentUrl?: string;
17
23
  title: string;
18
24
  }
19
25
 
26
+ type CopyStatus = "copied" | "error" | "idle";
27
+
20
28
  const LEADING_H1_REGEX = /^#\s+([^\r\n]+)(?:\r?\n(?:\r?\n)?)?/;
21
29
 
22
30
  const stripMatchingLeadingH1 = (source: string, title: string) => {
@@ -43,6 +51,37 @@ const formatMarkdownForCopy = (source: string, title: string) => {
43
51
  return `# ${title}\n\n${content}`;
44
52
  };
45
53
 
54
+ const getCopyLabel = (copyStatus: CopyStatus) => {
55
+ switch (copyStatus) {
56
+ case "copied": {
57
+ return "Copied";
58
+ }
59
+ case "error": {
60
+ return "Copy failed";
61
+ }
62
+ default: {
63
+ return "Copy page";
64
+ }
65
+ }
66
+ };
67
+
68
+ const getCopyDescription = (copyStatus: CopyStatus) => {
69
+ switch (copyStatus) {
70
+ case "copied": {
71
+ return "Copied page markdown to clipboard";
72
+ }
73
+ case "error": {
74
+ return "Clipboard access was blocked or the page markdown could not be loaded";
75
+ }
76
+ default: {
77
+ return "Copy page as Markdown for LLMs";
78
+ }
79
+ }
80
+ };
81
+
82
+ const getCopyIcon = (copyStatus: CopyStatus) =>
83
+ copyStatus === "copied" ? Checkmark1Icon : CopySimpleIcon;
84
+
46
85
  const MenuItem = ({
47
86
  children,
48
87
  href,
@@ -50,11 +89,11 @@ const MenuItem = ({
50
89
  }: {
51
90
  children: React.ReactNode;
52
91
  href?: string;
53
- onSelect?: () => void;
92
+ onSelect?: () => Promise<void> | void;
54
93
  }) =>
55
94
  href ? (
56
95
  <a
57
- className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors hover:bg-secondary/25"
96
+ className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors hover:bg-secondary/25 focus-visible:bg-secondary/25 focus-visible:outline-none"
58
97
  href={href}
59
98
  onClick={onSelect}
60
99
  rel="noopener noreferrer"
@@ -64,7 +103,7 @@ const MenuItem = ({
64
103
  </a>
65
104
  ) : (
66
105
  <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"
106
+ 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 focus-visible:bg-secondary/25 focus-visible:outline-none"
68
107
  onClick={onSelect}
69
108
  type="button"
70
109
  >
@@ -98,47 +137,48 @@ export const CopyPageMenu = ({
98
137
  contentUrl,
99
138
  title,
100
139
  }: CopyPageMenuProps) => {
101
- const menuRef = useRef<HTMLDivElement>(null);
102
- const [copied, setCopied] = useState(false);
140
+ const resetTimerRef = useRef<number | null>(null);
141
+ const [copyStatus, setCopyStatus] = useState<CopyStatus>("idle");
142
+ const [fetchedContent, setFetchedContent] = useState<string | null>(null);
103
143
  const [menuOpen, setMenuOpen] = useState(false);
104
144
  const [pageUrl, setPageUrl] = useState("");
105
- const [resolvedContent, setResolvedContent] = useState(content ?? "");
106
145
 
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;
146
+ useEffect(
147
+ () => () => {
148
+ if (resetTimerRef.current !== null) {
149
+ window.clearTimeout(resetTimerRef.current);
125
150
  }
126
- setMenuOpen(false);
127
- };
151
+ },
152
+ []
153
+ );
128
154
 
129
- document.addEventListener("mousedown", handlePointerDown);
130
- return () => document.removeEventListener("mousedown", handlePointerDown);
131
- }, [menuOpen]);
155
+ useEffect(() => {
156
+ setPageUrl(
157
+ new URL(contentUrl ?? window.location.href, window.location.href).href
158
+ );
159
+ }, [contentUrl]);
132
160
 
133
161
  const closeMenu = useCallback(() => {
134
162
  setMenuOpen(false);
135
163
  }, []);
136
164
 
137
- const toggleMenu = useCallback(() => {
138
- setMenuOpen((current) => !current);
139
- }, []);
165
+ const setTemporaryCopyStatus = useCallback(
166
+ (nextStatus: "copied" | "error") => {
167
+ if (resetTimerRef.current !== null) {
168
+ window.clearTimeout(resetTimerRef.current);
169
+ }
170
+
171
+ setCopyStatus(nextStatus);
172
+ resetTimerRef.current = window.setTimeout(() => {
173
+ setCopyStatus("idle");
174
+ resetTimerRef.current = null;
175
+ }, 2000);
176
+ },
177
+ []
178
+ );
140
179
 
141
180
  const getContent = useCallback(async () => {
181
+ const resolvedContent = content ?? fetchedContent;
142
182
  if (resolvedContent) {
143
183
  return resolvedContent;
144
184
  }
@@ -157,68 +197,73 @@ export const CopyPageMenu = ({
157
197
  }
158
198
 
159
199
  const nextContent = await response.text();
160
- setResolvedContent(nextContent);
200
+ setFetchedContent(nextContent);
161
201
  return nextContent;
162
- }, [contentUrl, resolvedContent]);
202
+ }, [content, contentUrl, fetchedContent]);
163
203
 
164
204
  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]);
205
+ try {
206
+ const nextContent = await getContent();
207
+ const markdown = formatMarkdownForCopy(nextContent, title);
208
+ await navigator.clipboard.writeText(markdown);
209
+ setTemporaryCopyStatus("copied");
210
+ closeMenu();
211
+ } catch {
212
+ setTemporaryCopyStatus("error");
213
+ }
214
+ }, [closeMenu, getContent, setTemporaryCopyStatus, title]);
172
215
 
173
216
  const chatgptUrl = pageUrl
174
217
  ? `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it.`)}`
175
- : "#";
218
+ : undefined;
176
219
  const claudeUrl = pageUrl
177
220
  ? `https://claude.ai/new?q=${encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it.`)}`
178
- : "#";
221
+ : undefined;
222
+ const copyLabel = getCopyLabel(copyStatus);
223
+ const copyDescription = getCopyDescription(copyStatus);
224
+ const CopyIcon = getCopyIcon(copyStatus);
179
225
 
180
226
  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>
227
+ <Popover onOpenChange={setMenuOpen} open={menuOpen}>
228
+ <div className="flex shrink-0 items-center">
229
+ <button
230
+ 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"
231
+ onClick={handleCopy}
232
+ type="button"
233
+ >
234
+ <CopyIcon aria-hidden="true" className="size-[18px]" />
235
+ <span>{copyLabel}</span>
236
+ </button>
237
+
238
+ <PopoverTrigger asChild>
239
+ <button
240
+ aria-expanded={menuOpen}
241
+ aria-label="More actions"
242
+ className="inline-flex items-center self-stretch rounded-r-xl border border-border px-2 transition-colors hover:bg-secondary/25"
243
+ type="button"
244
+ >
245
+ <ChevronDownSmallIcon
246
+ aria-hidden="true"
247
+ className="size-[18px] text-muted-foreground"
248
+ />
249
+ </button>
250
+ </PopoverTrigger>
251
+ </div>
252
+
253
+ <PopoverContent align="end" className="w-[280px] rounded-xl p-1">
254
+ <MenuItem onSelect={handleCopy}>
255
+ <MenuIcon>
256
+ <CopyIcon aria-hidden="true" className="size-[18px]" />
257
+ </MenuIcon>
258
+ <div>
259
+ <div className="font-medium">{copyLabel}</div>
260
+ <div className="text-xs text-muted-foreground">
261
+ {copyDescription}
219
262
  </div>
220
- </MenuItem>
263
+ </div>
264
+ </MenuItem>
221
265
 
266
+ {chatgptUrl ? (
222
267
  <MenuItem href={chatgptUrl} onSelect={closeMenu}>
223
268
  <MenuIcon>
224
269
  <OpenaiIcon aria-hidden="true" className="size-[18px]" />
@@ -233,7 +278,9 @@ export const CopyPageMenu = ({
233
278
  </div>
234
279
  </div>
235
280
  </MenuItem>
281
+ ) : null}
236
282
 
283
+ {claudeUrl ? (
237
284
  <MenuItem href={claudeUrl} onSelect={closeMenu}>
238
285
  <MenuIcon>
239
286
  <ClaudeaiIcon aria-hidden="true" className="size-[18px]" />
@@ -248,8 +295,8 @@ export const CopyPageMenu = ({
248
295
  </div>
249
296
  </div>
250
297
  </MenuItem>
251
- </div>
252
- ) : null}
253
- </div>
298
+ ) : null}
299
+ </PopoverContent>
300
+ </Popover>
254
301
  );
255
302
  };
@@ -9,6 +9,8 @@ import type { NavEntry, NavTab } from "@/lib/navigation";
9
9
  import { isExternalHref, resolveHref, toDocHref } from "@/lib/routes";
10
10
  import { cn } from "@/lib/utils";
11
11
 
12
+ const EMPTY_NAV: NavEntry[] = [];
13
+
12
14
  const Dropdown = ({
13
15
  label,
14
16
  items,
@@ -102,7 +104,7 @@ export const DocHeader = ({
102
104
  basePath,
103
105
  tabs,
104
106
  activeTabIndex,
105
- nav = [],
107
+ nav = EMPTY_NAV,
106
108
  }: {
107
109
  config: SiteConfig;
108
110
  basePath: string;
@@ -117,6 +119,10 @@ export const DocHeader = ({
117
119
  const [primaryLanguage] = languages;
118
120
  const searchDisabled = config.features?.search === false;
119
121
  const themeToggleDisabled = config.features?.themeToggle === false;
122
+ const logoHref = config.logo?.href ?? toDocHref("index", basePath);
123
+ const logoIsExternal = Boolean(
124
+ config.logo?.href && isExternalHref(config.logo.href)
125
+ );
120
126
 
121
127
  return (
122
128
  <header className="sticky top-0 z-50 w-full bg-background">
@@ -132,7 +138,9 @@ export const DocHeader = ({
132
138
  />
133
139
  <Link
134
140
  className="flex items-center gap-2.5"
135
- href={toDocHref("index", basePath)}
141
+ href={logoHref}
142
+ rel={logoIsExternal ? "noopener noreferrer" : undefined}
143
+ target={logoIsExternal ? "_blank" : undefined}
136
144
  >
137
145
  {config.logo?.light ? (
138
146
  <Image
@@ -186,7 +194,9 @@ export const DocHeader = ({
186
194
  ))}
187
195
  </nav>
188
196
  <div className="ml-auto flex items-center gap-2 md:flex-1 md:justify-end">
189
- {searchDisabled ? null : <Search basePath={basePath} />}
197
+ {searchDisabled ? null : (
198
+ <Search basePath={basePath} key={basePath} />
199
+ )}
190
200
  {primaryVersion ? (
191
201
  <Dropdown
192
202
  basePath={basePath}
@@ -27,13 +27,21 @@ import { themeStylesFromConfig } from "@/lib/theme";
27
27
  import type { TocItem } from "@/lib/toc";
28
28
  import { cn } from "@/lib/utils";
29
29
 
30
- const renderScripts = (
31
- scripts?: string[],
32
- strategy: "afterInteractive" | "lazyOnload" = "afterInteractive"
33
- ) =>
34
- scripts?.map((script) => (
30
+ const DocScripts = ({
31
+ scripts,
32
+ strategy = "afterInteractive",
33
+ }: {
34
+ scripts?: string[];
35
+ strategy?: "afterInteractive" | "lazyOnload";
36
+ }) => {
37
+ if (!scripts?.length) {
38
+ return null;
39
+ }
40
+
41
+ return scripts.map((script) => (
35
42
  <Script key={script} src={script} strategy={strategy} />
36
- )) ?? null;
43
+ ));
44
+ };
37
45
 
38
46
  const Breadcrumbs = ({
39
47
  basePath,
@@ -134,9 +142,10 @@ export const DocShell = ({
134
142
  (toc.length > 0 || (contextual && contextualDisplay === "toc"));
135
143
 
136
144
  const contextualTocItems =
137
- contextual && contextualDisplay === "toc" && rawContent !== undefined ? (
145
+ contextual && contextualDisplay === "toc" ? (
138
146
  <ContextualTocItems
139
147
  content={rawContent}
148
+ key={`toc-${currentPath}`}
140
149
  options={contextual.options}
141
150
  pagePath={currentPath}
142
151
  title={pageTitle}
@@ -144,9 +153,10 @@ export const DocShell = ({
144
153
  ) : null;
145
154
 
146
155
  const headerContextualMenu =
147
- contextual && contextualDisplay === "header" && rawContent !== undefined ? (
156
+ contextual && contextualDisplay === "header" ? (
148
157
  <ContextualMenu
149
158
  content={rawContent}
159
+ key={`header-${currentPath}`}
150
160
  options={contextual.options}
151
161
  pagePath={currentPath}
152
162
  title={pageTitle}
@@ -188,8 +198,9 @@ export const DocShell = ({
188
198
  (rawContent === undefined &&
189
199
  markdownHref === undefined ? null : (
190
200
  <CopyPageMenu
191
- content={rawContent}
201
+ content={markdownHref ? undefined : rawContent}
192
202
  contentUrl={markdownHref}
203
+ key={`copy-${currentPath}`}
193
204
  title={pageTitle}
194
205
  />
195
206
  ))}
@@ -275,7 +286,7 @@ export const DocShell = ({
275
286
  >
276
287
  Skip to content
277
288
  </a>
278
- {renderScripts(config.scripts?.head)}
289
+ <DocScripts scripts={config.scripts?.head} />
279
290
  <DocHeader
280
291
  activeTabIndex={activeTabIndex}
281
292
  basePath={basePath}
@@ -307,7 +318,7 @@ export const DocShell = ({
307
318
  </div>
308
319
  )}
309
320
  </div>
310
- {renderScripts(config.scripts?.body, "lazyOnload")}
321
+ <DocScripts scripts={config.scripts?.body} strategy="lazyOnload" />
311
322
  </div>
312
323
  );
313
324
  };
@@ -108,9 +108,6 @@ export const MobileNav = ({
108
108
  </div>
109
109
  <span className="sr-only">Toggle Menu</span>
110
110
  </div>
111
- <span className="flex h-8 items-center text-lg font-medium leading-none">
112
- Menu
113
- </span>
114
111
  </Button>
115
112
  </PopoverTrigger>
116
113
  <PopoverContent
@@ -164,9 +161,6 @@ export const MobileNav = ({
164
161
  ) : null}
165
162
  {globalLinks.length > 0 ? (
166
163
  <div className="flex flex-col gap-4">
167
- <div className="text-sm font-medium text-muted-foreground">
168
- Menu
169
- </div>
170
164
  <div className="flex flex-col gap-3">
171
165
  {globalLinks.map((link) => (
172
166
  <MobileLink