blodemd 0.0.8 → 0.0.10

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/README.md +25 -9
  2. package/dev-server/app/[[...slug]]/page.tsx +1 -0
  3. package/dev-server/app/favicon.ico +0 -0
  4. package/dev-server/next.config.js +11 -13
  5. package/dev-server/package.json +1 -1
  6. package/dev-server/tsconfig.json +3 -0
  7. package/dist/cli.mjs +869 -184
  8. package/dist/cli.mjs.map +1 -1
  9. package/docs/app/globals.css +1 -1
  10. package/docs/components/animate-ui/primitives/buttons/button.tsx +14 -0
  11. package/docs/components/api/api-playground.tsx +255 -80
  12. package/docs/components/api/api-reference.tsx +11 -1
  13. package/docs/components/docs/contextual-menu.tsx +227 -142
  14. package/docs/components/docs/copy-page-menu.tsx +148 -85
  15. package/docs/components/docs/doc-header.tsx +13 -3
  16. package/docs/components/docs/doc-shell.tsx +25 -14
  17. package/docs/components/docs/mobile-nav.tsx +0 -6
  18. package/docs/components/mdx/code-group.tsx +171 -62
  19. package/docs/components/mdx/steps.tsx +1 -1
  20. package/docs/components/mdx/tabs.tsx +131 -26
  21. package/docs/components/ui/copy-button.tsx +122 -0
  22. package/docs/components/ui/input.tsx +0 -1
  23. package/docs/components/ui/search.tsx +241 -132
  24. package/docs/components/ui/site-footer.tsx +39 -0
  25. package/docs/lib/config.ts +7 -0
  26. package/docs/lib/content-root.ts +33 -0
  27. package/docs/lib/content-source.ts +70 -0
  28. package/docs/lib/contextual-options.ts +20 -0
  29. package/docs/lib/docs-runtime.tsx +595 -0
  30. package/docs/lib/edge-config.ts +95 -0
  31. package/docs/lib/env.ts +22 -0
  32. package/docs/lib/openapi-proxy.ts +88 -0
  33. package/docs/lib/platform-config.ts +6 -0
  34. package/docs/lib/routes.ts +39 -0
  35. package/docs/lib/supabase.ts +13 -0
  36. package/docs/lib/tenancy.ts +350 -0
  37. package/docs/lib/tenant-headers.ts +14 -0
  38. package/docs/lib/tenant-static.ts +529 -0
  39. package/docs/lib/tenant-utility-context.ts +62 -0
  40. package/docs/lib/tenants.ts +68 -0
  41. package/docs/lib/use-mobile.ts +19 -0
  42. package/package.json +3 -2
  43. package/packages/@repo/common/dist/index.d.ts +7 -0
  44. package/packages/@repo/common/dist/index.d.ts.map +1 -1
  45. package/packages/@repo/common/dist/index.js +42 -0
  46. package/packages/@repo/common/src/index.ts +50 -0
  47. package/packages/@repo/contracts/dist/project.d.ts +1 -1
  48. package/packages/@repo/contracts/dist/project.js +1 -1
  49. package/packages/@repo/contracts/src/project.ts +1 -1
  50. package/packages/@repo/models/dist/docs-config.d.ts +194 -29
  51. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  52. package/packages/@repo/models/dist/docs-config.js +3 -28
  53. package/packages/@repo/models/src/docs-config.ts +5 -31
  54. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  55. package/packages/@repo/previewing/dist/blob-source.js +7 -2
  56. package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
  57. package/packages/@repo/previewing/dist/fs-source.js +2 -3
  58. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  59. package/packages/@repo/previewing/dist/index.js +20 -50
  60. package/packages/@repo/previewing/src/blob-source.ts +7 -4
  61. package/packages/@repo/previewing/src/fs-source.ts +2 -3
  62. package/packages/@repo/previewing/src/index.ts +29 -64
  63. package/packages/@repo/validation/dist/index.d.ts +2 -2
  64. package/packages/@repo/validation/dist/index.d.ts.map +1 -1
  65. package/packages/@repo/validation/dist/index.js +2 -2
  66. package/packages/@repo/validation/package.json +1 -0
  67. package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
  68. package/packages/@repo/validation/src/index.ts +4 -4
@@ -22,6 +22,11 @@ import {
22
22
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
23
23
  import type { ComponentType, ReactNode, SVGProps } from "react";
24
24
 
25
+ import {
26
+ Popover,
27
+ PopoverContent,
28
+ PopoverTrigger,
29
+ } from "@/components/ui/popover";
25
30
  import {
26
31
  buildBuiltinUrl,
27
32
  builtinOptions,
@@ -29,6 +34,29 @@ import {
29
34
  } from "@/lib/contextual-options";
30
35
 
31
36
  type IconComponent = ComponentType<SVGProps<SVGSVGElement>>;
37
+ type ActionId = "add-mcp" | "assistant" | "copy" | "mcp";
38
+
39
+ interface ResolvedOption {
40
+ key: string;
41
+ title: string;
42
+ description: string;
43
+ icon: IconComponent;
44
+ type: "action" | "link";
45
+ action?: ActionId;
46
+ href?: string;
47
+ }
48
+
49
+ interface ContextualContext {
50
+ pageUrl: string;
51
+ pageContent: string;
52
+ pagePath: string;
53
+ mcpServerUrl?: string;
54
+ }
55
+
56
+ interface ActionFeedback {
57
+ id: string;
58
+ state: "copied" | "error";
59
+ }
32
60
 
33
61
  const iconMap: Record<string, IconComponent> = {
34
62
  ClaudeaiIcon,
@@ -50,36 +78,64 @@ const iconMap: Record<string, IconComponent> = {
50
78
  const getBuiltinIcon = (iconName: string): IconComponent =>
51
79
  iconMap[iconName] ?? CopySimpleIcon;
52
80
 
53
- type ActionId = "copy" | "mcp" | "add-mcp" | "assistant";
81
+ const getFeedbackLabel = (
82
+ feedbackState: ActionFeedback["state"] | null,
83
+ defaultLabel: string
84
+ ) => {
85
+ switch (feedbackState) {
86
+ case "copied": {
87
+ return "Copied";
88
+ }
89
+ case "error": {
90
+ return "Copy failed";
91
+ }
92
+ default: {
93
+ return defaultLabel;
94
+ }
95
+ }
96
+ };
54
97
 
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
- }
98
+ const getFeedbackDescription = (
99
+ feedbackState: ActionFeedback["state"] | null,
100
+ defaultDescription: string
101
+ ) => {
102
+ switch (feedbackState) {
103
+ case "copied": {
104
+ return "Copied to clipboard";
105
+ }
106
+ case "error": {
107
+ return "Clipboard access was blocked";
108
+ }
109
+ default: {
110
+ return defaultDescription;
111
+ }
112
+ }
113
+ };
64
114
 
65
- interface ContextualContext {
66
- pageUrl: string;
67
- pageContent: string;
68
- pagePath: string;
69
- mcpServerUrl?: string;
70
- }
115
+ const getFeedbackIcon = (
116
+ feedbackState: ActionFeedback["state"] | null,
117
+ defaultIcon: IconComponent
118
+ ) => {
119
+ if (feedbackState === "copied") {
120
+ return Checkmark1Icon;
121
+ }
122
+
123
+ return defaultIcon;
124
+ };
71
125
 
72
126
  const resolveOptions = (
73
127
  options: ContextualOption[],
74
128
  context: ContextualContext
75
129
  ): ResolvedOption[] => {
76
130
  const resolved: ResolvedOption[] = [];
131
+
77
132
  for (const option of options) {
78
133
  if (typeof option === "string") {
79
134
  const definition = builtinOptions[option];
80
135
  if (!definition) {
81
136
  continue;
82
137
  }
138
+
83
139
  if (definition.type === "action") {
84
140
  resolved.push({
85
141
  action: option as ActionId,
@@ -89,59 +145,96 @@ const resolveOptions = (
89
145
  title: definition.title,
90
146
  type: "action",
91
147
  });
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
- }
148
+ continue;
104
149
  }
105
- } else {
150
+
151
+ const href = buildBuiltinUrl(option, context);
152
+ if (!href) {
153
+ continue;
154
+ }
155
+
106
156
  resolved.push({
107
- description: option.description,
108
- href: resolveCustomHref(option.href, context),
109
- icon: CopySimpleIcon,
110
- key: `custom-${option.title}`,
111
- title: option.title,
157
+ description: definition.description,
158
+ href,
159
+ icon: getBuiltinIcon(definition.iconName),
160
+ key: option,
161
+ title: definition.title,
112
162
  type: "link",
113
163
  });
164
+ continue;
114
165
  }
166
+
167
+ resolved.push({
168
+ description: option.description,
169
+ href: resolveCustomHref(option.href, context),
170
+ icon: CopySimpleIcon,
171
+ key: `custom-${option.title}`,
172
+ title: option.title,
173
+ type: "link",
174
+ });
115
175
  }
176
+
116
177
  return resolved;
117
178
  };
118
179
 
119
- const useContextualActions = (content: string, title: string) => {
120
- const [copiedId, setCopiedId] = useState<string | null>(null);
180
+ const useContextualActions = (content: string | undefined, title: string) => {
181
+ const resetTimerRef = useRef<number | null>(null);
182
+ const [feedback, setFeedback] = useState<ActionFeedback | null>(null);
183
+
184
+ useEffect(
185
+ () => () => {
186
+ if (resetTimerRef.current !== null) {
187
+ window.clearTimeout(resetTimerRef.current);
188
+ }
189
+ },
190
+ []
191
+ );
192
+
193
+ const setActionFeedback = useCallback(
194
+ (id: string, state: ActionFeedback["state"]) => {
195
+ if (resetTimerRef.current !== null) {
196
+ window.clearTimeout(resetTimerRef.current);
197
+ }
198
+
199
+ setFeedback({ id, state });
200
+ resetTimerRef.current = window.setTimeout(() => {
201
+ setFeedback(null);
202
+ resetTimerRef.current = null;
203
+ }, 2000);
204
+ },
205
+ []
206
+ );
121
207
 
122
208
  const handleAction = useCallback(
123
209
  async (action: string, key?: string) => {
124
210
  const id = key ?? action;
211
+
125
212
  switch (action) {
126
213
  case "copy": {
127
- await navigator.clipboard.writeText(`# ${title}\n\n${content}`);
128
- setCopiedId(id);
129
- setTimeout(() => setCopiedId(null), 2000);
130
- break;
214
+ try {
215
+ await navigator.clipboard.writeText(
216
+ `# ${title}\n\n${content ?? ""}`
217
+ );
218
+ setActionFeedback(id, "copied");
219
+ return true;
220
+ } catch {
221
+ setActionFeedback(id, "error");
222
+ return false;
223
+ }
131
224
  }
132
225
  default: {
133
- break;
226
+ return true;
134
227
  }
135
228
  }
136
229
  },
137
- [content, title]
230
+ [content, setActionFeedback, title]
138
231
  );
139
232
 
140
- return { copiedId, handleAction };
233
+ return { feedback, handleAction };
141
234
  };
142
235
 
143
236
  const usePageContext = (
144
- content: string,
237
+ content: string | undefined,
145
238
  title: string,
146
239
  pagePath: string
147
240
  ): ContextualContext => {
@@ -149,14 +242,17 @@ const usePageContext = (
149
242
 
150
243
  useEffect(() => {
151
244
  setPageUrl(window.location.href);
152
- }, []);
153
-
154
- return {
155
- mcpServerUrl: undefined,
156
- pageContent: `# ${title}\n\n${content}`,
157
- pagePath,
158
- pageUrl,
159
- };
245
+ }, [pagePath]);
246
+
247
+ return useMemo(
248
+ () => ({
249
+ mcpServerUrl: undefined,
250
+ pageContent: `# ${title}\n\n${content ?? ""}`,
251
+ pagePath,
252
+ pageUrl,
253
+ }),
254
+ [content, pagePath, pageUrl, title]
255
+ );
160
256
  };
161
257
 
162
258
  const MenuIcon = ({ children }: { children: ReactNode }) => (
@@ -172,11 +268,11 @@ const MenuItem = ({
172
268
  }: {
173
269
  children: ReactNode;
174
270
  href?: string;
175
- onSelect?: () => void;
271
+ onSelect?: () => Promise<void> | void;
176
272
  }) =>
177
273
  href ? (
178
274
  <a
179
- className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors hover:bg-secondary/25"
275
+ 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"
180
276
  href={href}
181
277
  onClick={onSelect}
182
278
  rel="noopener noreferrer"
@@ -186,7 +282,7 @@ const MenuItem = ({
186
282
  </a>
187
283
  ) : (
188
284
  <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"
285
+ 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"
190
286
  onClick={onSelect}
191
287
  type="button"
192
288
  >
@@ -211,7 +307,7 @@ const ExternalArrow = () => (
211
307
 
212
308
  interface ContextualMenuProps {
213
309
  options: ContextualOption[];
214
- content: string;
310
+ content?: string;
215
311
  title: string;
216
312
  pagePath: string;
217
313
  }
@@ -222,42 +318,21 @@ export const ContextualMenu = ({
222
318
  title,
223
319
  pagePath,
224
320
  }: ContextualMenuProps) => {
225
- const menuRef = useRef<HTMLDivElement>(null);
226
321
  const context = usePageContext(content, title, pagePath);
227
- const { copiedId, handleAction } = useContextualActions(content, title);
322
+ const { feedback, handleAction } = useContextualActions(content, title);
228
323
  const [menuOpen, setMenuOpen] = useState(false);
229
324
 
230
325
  const resolved = useMemo(
231
326
  () => resolveOptions(options, context),
232
327
  [context, options]
233
328
  );
234
-
235
329
  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]);
330
+ const hasSecondaryOptions = resolved.length > 1;
252
331
 
253
332
  const closeMenu = useCallback(() => {
254
333
  setMenuOpen(false);
255
334
  }, []);
256
335
 
257
- const toggleMenu = useCallback(() => {
258
- setMenuOpen((current) => !current);
259
- }, []);
260
-
261
336
  const handlePrimaryAction = useCallback(async () => {
262
337
  if (!primaryOption) {
263
338
  return;
@@ -279,11 +354,16 @@ export const ContextualMenu = ({
279
354
  .map((item) => [
280
355
  item.key,
281
356
  async () => {
282
- await handleAction(item.action ?? "", item.key);
283
- closeMenu();
357
+ const didComplete = await handleAction(
358
+ item.action ?? "",
359
+ item.key
360
+ );
361
+ if (didComplete) {
362
+ closeMenu();
363
+ }
284
364
  },
285
365
  ])
286
- ),
366
+ ) as Record<string, () => Promise<void>>,
287
367
  [closeMenu, handleAction, resolved]
288
368
  );
289
369
 
@@ -291,34 +371,38 @@ export const ContextualMenu = ({
291
371
  return null;
292
372
  }
293
373
 
294
- const isCopied = copiedId === primaryOption.key;
374
+ const primaryFeedback =
375
+ feedback?.id === primaryOption.key ? feedback.state : null;
376
+ const primaryLabel = getFeedbackLabel(primaryFeedback, primaryOption.title);
377
+ const PrimaryIcon = getFeedbackIcon(primaryFeedback, primaryOption.icon);
378
+ const primaryButtonClassName = hasSecondaryOptions
379
+ ? "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"
380
+ : "inline-flex items-center gap-2 rounded-xl border border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-secondary/25";
381
+ const primaryButton = (
382
+ <button
383
+ className={primaryButtonClassName}
384
+ onClick={handlePrimaryAction}
385
+ type="button"
386
+ >
387
+ <PrimaryIcon aria-hidden="true" className="size-[18px]" />
388
+ <span>{primaryLabel}</span>
389
+ </button>
390
+ );
391
+
392
+ if (!hasSecondaryOptions) {
393
+ return <div className="flex shrink-0 items-center">{primaryButton}</div>;
394
+ }
295
395
 
296
396
  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
- <>
397
+ <Popover onOpenChange={setMenuOpen} open={menuOpen}>
398
+ <div className="flex shrink-0 items-center">
399
+ {primaryButton}
400
+
401
+ <PopoverTrigger asChild>
317
402
  <button
318
403
  aria-expanded={menuOpen}
319
404
  aria-label="More actions"
320
405
  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
406
  type="button"
323
407
  >
324
408
  <ChevronDownSmallIcon
@@ -326,44 +410,45 @@ export const ContextualMenu = ({
326
410
  className="size-[18px] text-muted-foreground"
327
411
  />
328
412
  </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>
413
+ </PopoverTrigger>
414
+ </div>
415
+
416
+ <PopoverContent align="end" className="w-[320px] rounded-xl p-1">
417
+ {resolved.slice(1).map((item) => {
418
+ const itemFeedback =
419
+ feedback?.id === item.key ? feedback.state : null;
420
+ const itemLabel = getFeedbackLabel(itemFeedback, item.title);
421
+ const itemDescription = getFeedbackDescription(
422
+ itemFeedback,
423
+ item.description
424
+ );
425
+ const ItemIcon = getFeedbackIcon(itemFeedback, item.icon);
426
+
427
+ return (
428
+ <MenuItem
429
+ href={item.type === "link" ? item.href : undefined}
430
+ key={item.key}
431
+ onSelect={
432
+ item.type === "action" ? actionHandlers[item.key] : closeMenu
433
+ }
434
+ >
435
+ <MenuIcon>
436
+ <ItemIcon aria-hidden="true" className="size-[18px]" />
437
+ </MenuIcon>
438
+ <div className="flex-1">
439
+ <div className="font-medium">
440
+ {itemLabel}
441
+ {item.type === "link" ? <ExternalArrow /> : null}
442
+ </div>
443
+ <div className="text-xs text-muted-foreground">
444
+ {itemDescription}
445
+ </div>
446
+ </div>
447
+ </MenuItem>
448
+ );
449
+ })}
450
+ </PopoverContent>
451
+ </Popover>
367
452
  );
368
453
  };
369
454