blodemd 0.0.13 → 0.0.15

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 (136) hide show
  1. package/README.md +1 -1
  2. package/dev-server/lib/local-runtime.tsx +12 -15
  3. package/dev-server/package.json +5 -1
  4. package/dist/cli.mjs +1211 -983
  5. package/dist/cli.mjs.map +1 -1
  6. package/docs/app/globals.css +1 -1
  7. package/docs/components/api/api-playground.tsx +2 -5
  8. package/docs/components/api/api-reference.tsx +29 -3
  9. package/docs/components/docs/contextual-menu.tsx +37 -33
  10. package/docs/components/docs/copy-page-menu.tsx +18 -0
  11. package/docs/components/docs/doc-header.tsx +4 -1
  12. package/docs/components/docs/doc-shell.tsx +48 -18
  13. package/docs/components/docs/doc-sidebar.tsx +131 -89
  14. package/docs/components/docs/doc-toc.tsx +1 -0
  15. package/docs/components/docs/mobile-nav.tsx +72 -68
  16. package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
  17. package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
  18. package/docs/components/git/repo-picker.tsx +5 -2
  19. package/docs/components/mdx/index.tsx +31 -21
  20. package/docs/components/posthog-provider.tsx +42 -0
  21. package/docs/components/tenant-analytics.tsx +49 -0
  22. package/docs/components/third-parties.tsx +4 -1
  23. package/docs/components/ui/copy-button.tsx +16 -2
  24. package/docs/components/ui/search-dialog.tsx +431 -0
  25. package/docs/components/ui/search.tsx +44 -427
  26. package/docs/components/web-mcp.tsx +115 -0
  27. package/docs/lib/agent-skills.ts +48 -0
  28. package/docs/lib/config.ts +4 -5
  29. package/docs/lib/docs-collection.ts +7 -22
  30. package/docs/lib/docs-runtime.tsx +160 -10
  31. package/docs/lib/llms-full.ts +82 -0
  32. package/docs/lib/marketing-markdown.ts +79 -0
  33. package/docs/lib/marketing-site.ts +31 -0
  34. package/docs/lib/mdx.ts +17 -6
  35. package/docs/lib/routes.ts +19 -3
  36. package/docs/lib/shiki.ts +4 -0
  37. package/docs/lib/tenancy.ts +15 -2
  38. package/docs/lib/tenant-headers.ts +29 -0
  39. package/docs/lib/tenant-static.ts +73 -99
  40. package/package.json +8 -5
  41. package/packages/@repo/common/package.json +1 -1
  42. package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
  43. package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
  44. package/packages/@repo/contracts/dist/analytics.js +18 -0
  45. package/packages/@repo/contracts/dist/deployment.js +1 -1
  46. package/packages/@repo/contracts/dist/git.js +2 -2
  47. package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
  48. package/packages/@repo/contracts/dist/ids.js +2 -1
  49. package/packages/@repo/contracts/dist/index.d.ts +1 -0
  50. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  51. package/packages/@repo/contracts/dist/index.js +1 -0
  52. package/packages/@repo/contracts/dist/project.d.ts +18 -1
  53. package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
  54. package/packages/@repo/contracts/dist/project.js +3 -1
  55. package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
  56. package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
  57. package/packages/@repo/contracts/dist/tenant.js +2 -0
  58. package/packages/@repo/contracts/package.json +1 -1
  59. package/packages/@repo/contracts/src/analytics.ts +33 -0
  60. package/packages/@repo/contracts/src/deployment.ts +1 -1
  61. package/packages/@repo/contracts/src/git.ts +2 -2
  62. package/packages/@repo/contracts/src/ids.ts +2 -1
  63. package/packages/@repo/contracts/src/index.ts +1 -0
  64. package/packages/@repo/contracts/src/project.ts +3 -1
  65. package/packages/@repo/contracts/src/tenant.ts +2 -0
  66. package/packages/@repo/models/dist/docs-config.d.ts +0 -198
  67. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  68. package/packages/@repo/models/dist/docs-config.js +3 -2
  69. package/packages/@repo/models/dist/tenant.d.ts +12 -0
  70. package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
  71. package/packages/@repo/models/package.json +1 -1
  72. package/packages/@repo/models/src/docs-config.ts +3 -3
  73. package/packages/@repo/models/src/tenant.ts +15 -0
  74. package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
  75. package/packages/@repo/prebuild/dist/openapi.js +32 -4
  76. package/packages/@repo/prebuild/package.json +1 -1
  77. package/packages/@repo/prebuild/src/openapi.ts +42 -4
  78. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  79. package/packages/@repo/previewing/dist/blob-source.js +18 -5
  80. package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
  81. package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
  82. package/packages/@repo/previewing/dist/config-loader.js +231 -0
  83. package/packages/@repo/previewing/dist/constants.d.ts +15 -0
  84. package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
  85. package/packages/@repo/previewing/dist/constants.js +14 -0
  86. package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
  87. package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
  88. package/packages/@repo/previewing/dist/content-index.js +291 -0
  89. package/packages/@repo/previewing/dist/index.d.ts +13 -121
  90. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  91. package/packages/@repo/previewing/dist/index.js +11 -1003
  92. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
  93. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
  94. package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
  95. package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
  96. package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
  97. package/packages/@repo/previewing/dist/markdown/format.js +43 -0
  98. package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
  99. package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
  100. package/packages/@repo/previewing/dist/markdown/links.js +31 -0
  101. package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
  102. package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
  103. package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
  104. package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
  105. package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
  106. package/packages/@repo/previewing/dist/search-index.js +23 -0
  107. package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
  108. package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
  109. package/packages/@repo/previewing/dist/serialization.js +108 -0
  110. package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
  111. package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
  112. package/packages/@repo/previewing/dist/toc-index.js +53 -0
  113. package/packages/@repo/previewing/dist/types.d.ts +92 -0
  114. package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
  115. package/packages/@repo/previewing/dist/types.js +1 -0
  116. package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
  117. package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
  118. package/packages/@repo/previewing/dist/utility-index.js +201 -0
  119. package/packages/@repo/previewing/package.json +1 -1
  120. package/packages/@repo/previewing/src/blob-source.ts +20 -5
  121. package/packages/@repo/previewing/src/config-loader.ts +316 -0
  122. package/packages/@repo/previewing/src/constants.ts +16 -0
  123. package/packages/@repo/previewing/src/content-index.ts +384 -0
  124. package/packages/@repo/previewing/src/index.ts +69 -1515
  125. package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
  126. package/packages/@repo/previewing/src/markdown/format.ts +63 -0
  127. package/packages/@repo/previewing/src/markdown/links.ts +46 -0
  128. package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
  129. package/packages/@repo/previewing/src/search-index.ts +38 -0
  130. package/packages/@repo/previewing/src/serialization.ts +191 -0
  131. package/packages/@repo/previewing/src/toc-index.ts +72 -0
  132. package/packages/@repo/previewing/src/types.ts +105 -0
  133. package/packages/@repo/previewing/src/utility-index.ts +304 -0
  134. package/packages/@repo/validation/package.json +1 -1
  135. package/scripts/repo-packages.mjs +8 -0
  136. package/docs/components/ui/site-footer.tsx +0 -41
@@ -109,82 +109,86 @@ export const MobileNav = ({
109
109
  side="bottom"
110
110
  sideOffset={14}
111
111
  >
112
- <div className="flex flex-col gap-12 overflow-auto px-6 py-6">
113
- {tabs?.length ? (
114
- <div className="flex flex-col gap-4">
115
- <div className="text-sm font-medium text-muted-foreground">
116
- Sections
117
- </div>
118
- <div className="flex flex-col gap-3">
119
- {tabs.map((tab, index) => {
120
- const href =
121
- (tab.href ? resolveHref(tab.href, basePath) : undefined) ??
122
- (tab.slugPrefix
123
- ? toDocHref(tab.slugPrefix, basePath)
124
- : undefined);
125
- if (!href) {
126
- return null;
127
- }
128
- const isActive = index === activeTabIndex;
112
+ {open ? (
113
+ <div className="flex flex-col gap-12 overflow-auto px-6 py-6">
114
+ {tabs?.length ? (
115
+ <div className="flex flex-col gap-4">
116
+ <div className="text-sm font-medium text-muted-foreground">
117
+ Sections
118
+ </div>
119
+ <div className="flex flex-col gap-3">
120
+ {tabs.map((tab, index) => {
121
+ const href =
122
+ (tab.href
123
+ ? resolveHref(tab.href, basePath)
124
+ : undefined) ??
125
+ (tab.slugPrefix
126
+ ? toDocHref(tab.slugPrefix, basePath)
127
+ : undefined);
128
+ if (!href) {
129
+ return null;
130
+ }
131
+ const isActive = index === activeTabIndex;
129
132
 
130
- return (
133
+ return (
134
+ <MobileLink
135
+ className={isActive ? "text-primary" : ""}
136
+ href={href}
137
+ key={tab.label}
138
+ onOpenChange={setOpen}
139
+ rel={
140
+ tab.href && isExternalHref(tab.href)
141
+ ? "noopener noreferrer"
142
+ : undefined
143
+ }
144
+ target={
145
+ tab.href && isExternalHref(tab.href)
146
+ ? "_blank"
147
+ : undefined
148
+ }
149
+ >
150
+ {tab.label}
151
+ </MobileLink>
152
+ );
153
+ })}
154
+ </div>
155
+ </div>
156
+ ) : null}
157
+ {globalLinks.length > 0 ? (
158
+ <div className="flex flex-col gap-4">
159
+ <div className="flex flex-col gap-3">
160
+ {globalLinks.map((link) => (
131
161
  <MobileLink
132
- className={isActive ? "text-primary" : ""}
133
- href={href}
134
- key={tab.label}
162
+ href={link.href}
163
+ key={link.label}
135
164
  onOpenChange={setOpen}
136
- rel={
137
- tab.href && isExternalHref(tab.href)
138
- ? "noopener noreferrer"
139
- : undefined
140
- }
141
- target={
142
- tab.href && isExternalHref(tab.href)
143
- ? "_blank"
144
- : undefined
145
- }
165
+ rel="noopener noreferrer"
166
+ target="_blank"
146
167
  >
147
- {tab.label}
168
+ {link.label}
148
169
  </MobileLink>
149
- );
150
- })}
151
- </div>
152
- </div>
153
- ) : null}
154
- {globalLinks.length > 0 ? (
155
- <div className="flex flex-col gap-4">
156
- <div className="flex flex-col gap-3">
157
- {globalLinks.map((link) => (
158
- <MobileLink
159
- href={link.href}
160
- key={link.label}
161
- onOpenChange={setOpen}
162
- rel="noopener noreferrer"
163
- target="_blank"
164
- >
165
- {link.label}
166
- </MobileLink>
167
- ))}
170
+ ))}
171
+ </div>
168
172
  </div>
169
- </div>
170
- ) : null}
171
- {entries.map((entry) => {
172
- if (entry.type === "page") {
173
- return renderPageLink(entry);
174
- }
173
+ ) : null}
174
+ {entries.map((entry) => {
175
+ if (entry.type === "page") {
176
+ return renderPageLink(entry);
177
+ }
175
178
 
176
- return (
177
- <div className="flex flex-col gap-4" key={entry.title}>
178
- <div className="text-sm font-medium text-muted-foreground">
179
- {entry.title}
179
+ return (
180
+ <div className="flex flex-col gap-4" key={entry.title}>
181
+ <div className="text-sm font-medium text-muted-foreground">
182
+ {entry.title}
183
+ </div>
184
+ <div className="flex flex-col gap-3">
185
+ {entry.items.map((item) => renderPageLink(item))}
186
+ </div>
180
187
  </div>
181
- <div className="flex flex-col gap-3">
182
- {entry.items.map((item) => renderPageLink(item))}
183
- </div>
184
- </div>
185
- );
186
- })}
187
- </div>
188
+ );
189
+ })}
190
+ </div>
191
+ ) : null}
188
192
  </PopoverContent>
189
193
  </Popover>
190
194
  );
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { normalizePath } from "@repo/common";
4
+ import { usePathname } from "next/navigation";
5
+ import { useEffect } from "react";
6
+
7
+ const ACTIVE_CLASSES = ["border-accent", "bg-accent", "text-foreground"];
8
+
9
+ export const SidebarActiveHighlight = ({ basePath }: { basePath: string }) => {
10
+ const pathname = usePathname();
11
+
12
+ useEffect(() => {
13
+ const stripped = basePath
14
+ ? pathname.replace(new RegExp(`^${basePath}`), "")
15
+ : pathname;
16
+ const target = normalizePath(stripped) || "index";
17
+
18
+ const links = document.querySelectorAll<HTMLElement>(
19
+ "[data-sidebar-link][data-path]"
20
+ );
21
+ for (const link of links) {
22
+ const isActive = link.dataset.path === target;
23
+ if (isActive) {
24
+ link.dataset.active = "true";
25
+ link.classList.add(...ACTIVE_CLASSES);
26
+ } else {
27
+ delete link.dataset.active;
28
+ link.classList.remove(...ACTIVE_CLASSES);
29
+ }
30
+ }
31
+ }, [basePath, pathname]);
32
+
33
+ return null;
34
+ };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useLayoutEffect, useRef } from "react";
3
+ import { useEffect, useRef } from "react";
4
4
 
5
5
  let savedScrollTop = 0;
6
6
 
@@ -13,7 +13,7 @@ export const SidebarScrollArea = ({
13
13
  }) => {
14
14
  const scrollRef = useRef<HTMLDivElement>(null);
15
15
 
16
- useLayoutEffect(() => {
16
+ useEffect(() => {
17
17
  const el = scrollRef.current;
18
18
  if (!el) {
19
19
  return;
@@ -21,12 +21,17 @@ export const SidebarScrollArea = ({
21
21
 
22
22
  if (savedScrollTop > 0) {
23
23
  el.scrollTop = savedScrollTop;
24
- } else {
24
+ return;
25
+ }
26
+
27
+ // Defer to allow SidebarActiveHighlight to mark the active link first.
28
+ const id = requestAnimationFrame(() => {
25
29
  const active = el.querySelector<HTMLElement>("[data-active]");
26
30
  if (active) {
27
31
  active.scrollIntoView({ behavior: "instant", block: "center" });
28
32
  }
29
- }
33
+ });
34
+ return () => cancelAnimationFrame(id);
30
35
  });
31
36
 
32
37
  useEffect(() => {
@@ -49,13 +49,13 @@ import { GITHUB_INSTALL_STATE_KEY } from "@/lib/github-install";
49
49
  import { timeAgo } from "@/lib/time-ago";
50
50
  import { cn } from "@/lib/utils";
51
51
 
52
- export interface RepoPickerInstallation {
52
+ interface RepoPickerInstallation {
53
53
  id: number;
54
54
  accountLogin: string;
55
55
  accountType: string;
56
56
  }
57
57
 
58
- export interface RepoPickerProps {
58
+ interface RepoPickerProps {
59
59
  accessToken: string;
60
60
  installations: RepoPickerInstallation[];
61
61
  onAddAccount: () => void;
@@ -387,6 +387,9 @@ export const RepoPicker = ({
387
387
  const handlePick = (repo: RepoSummary) => {
388
388
  setSelected(repo);
389
389
  setBranch(repo.defaultBranch || "main");
390
+ // Reset the docs path so an edit made for a previous repo doesn't carry
391
+ // over to a freshly picked one.
392
+ setDocsPath("docs");
390
393
  setError(null);
391
394
  };
392
395
 
@@ -2,6 +2,8 @@ import type { MDXComponents } from "mdx/types";
2
2
  import Link from "next/link";
3
3
  import type { ComponentProps } from "react";
4
4
 
5
+ import { isExternalHref, resolveHref } from "@/lib/routes";
6
+
5
7
  import { Accordion, AccordionGroup } from "./accordion";
6
8
  import { AgentInstructions } from "./agent-instructions";
7
9
  import { Badge } from "./badge";
@@ -42,30 +44,36 @@ const Tree = Object.assign(
42
44
  }
43
45
  );
44
46
 
45
- const MdxLink = ({
46
- href,
47
- children,
48
- ...props
49
- }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
50
- if (!href) {
51
- return <a {...props}>{children}</a>;
52
- }
53
- const isExternal = href.startsWith("http");
54
- if (isExternal) {
47
+ const createMdxLink = (basePath: string, currentPath: string) => {
48
+ const MdxLink = ({
49
+ href,
50
+ children,
51
+ ...props
52
+ }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
53
+ if (!href) {
54
+ return <a {...props}>{children}</a>;
55
+ }
56
+ if (isExternalHref(href)) {
57
+ return (
58
+ <a {...props} href={href} rel="noopener noreferrer" target="_blank">
59
+ {children}
60
+ </a>
61
+ );
62
+ }
55
63
  return (
56
- <a {...props} href={href} rel="noopener noreferrer" target="_blank">
64
+ <Link href={resolveHref(href, basePath, currentPath)} {...props}>
57
65
  {children}
58
- </a>
66
+ </Link>
59
67
  );
60
- }
61
- return (
62
- <Link href={href} {...props}>
63
- {children}
64
- </Link>
65
- );
68
+ };
69
+ MdxLink.displayName = "MdxLink";
70
+ return MdxLink;
66
71
  };
67
72
 
68
- export const mdxComponents: MDXComponents = {
73
+ export const createMdxComponents = (
74
+ basePath = "",
75
+ currentPath = ""
76
+ ): MDXComponents => ({
69
77
  Accordion,
70
78
  AccordionGroup,
71
79
  AgentInstructions,
@@ -106,6 +114,8 @@ export const mdxComponents: MDXComponents = {
106
114
  View,
107
115
  ViewGroup,
108
116
  Warning,
109
- a: MdxLink,
117
+ a: createMdxLink(basePath, currentPath),
110
118
  pre: CodeBlock,
111
- };
119
+ });
120
+
121
+ export const mdxComponents: MDXComponents = createMdxComponents();
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { usePathname, useSearchParams } from "next/navigation";
4
+ import posthogJs from "posthog-js";
5
+ import { useEffect } from "react";
6
+
7
+ const DEFAULT_POSTHOG_HOST = "https://us.i.posthogJs.com";
8
+
9
+ interface PostHogProviderProps {
10
+ projectKey: string;
11
+ host?: string;
12
+ }
13
+
14
+ export const PostHogProvider = ({ projectKey, host }: PostHogProviderProps) => {
15
+ useEffect(() => {
16
+ if (!projectKey) {
17
+ return;
18
+ }
19
+ if (posthogJs.__loaded) {
20
+ return;
21
+ }
22
+ posthogJs.init(projectKey, {
23
+ api_host: host || DEFAULT_POSTHOG_HOST,
24
+ capture_pageleave: true,
25
+ capture_pageview: false,
26
+ person_profiles: "identified_only",
27
+ });
28
+ }, [projectKey, host]);
29
+
30
+ const pathname = usePathname();
31
+ const searchParams = useSearchParams();
32
+ useEffect(() => {
33
+ if (!(pathname && posthogJs.__loaded)) {
34
+ return;
35
+ }
36
+ const query = searchParams?.toString();
37
+ const url = query ? `${pathname}?${query}` : pathname;
38
+ posthogJs.capture("$pageview", { $current_url: url });
39
+ }, [pathname, searchParams]);
40
+
41
+ return null;
42
+ };
@@ -0,0 +1,49 @@
1
+ import { GoogleAnalytics } from "@next/third-parties/google";
2
+ import { headers } from "next/headers";
3
+ import Script from "next/script";
4
+ import { Suspense } from "react";
5
+
6
+ import {
7
+ decodeTenantAnalyticsHeader,
8
+ TENANT_HEADERS,
9
+ } from "@/lib/tenant-headers";
10
+
11
+ import { PostHogProvider } from "./posthog-provider";
12
+
13
+ // Consent Mode v2 default-denied bootstrap. Replace with a real CMP when ready.
14
+ const CONSENT_DEFAULTS_SCRIPT = `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('consent','default',{ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied',analytics_storage:'denied',wait_for_update:500});`;
15
+
16
+ export const TenantAnalytics = async () => {
17
+ if (process.env.VERCEL_ENV !== "production") {
18
+ return null;
19
+ }
20
+
21
+ const headerStore = await headers();
22
+ const analytics = decodeTenantAnalyticsHeader(
23
+ headerStore.get(TENANT_HEADERS.ANALYTICS)
24
+ );
25
+ if (!analytics) {
26
+ return null;
27
+ }
28
+
29
+ return (
30
+ <>
31
+ {analytics.ga4?.measurementId ? (
32
+ <>
33
+ <Script id="ga-consent-defaults" strategy="afterInteractive">
34
+ {CONSENT_DEFAULTS_SCRIPT}
35
+ </Script>
36
+ <GoogleAnalytics gaId={analytics.ga4.measurementId} />
37
+ </>
38
+ ) : null}
39
+ {analytics.posthog?.projectKey ? (
40
+ <Suspense fallback={null}>
41
+ <PostHogProvider
42
+ host={analytics.posthog.host}
43
+ projectKey={analytics.posthog.projectKey}
44
+ />
45
+ </Suspense>
46
+ ) : null}
47
+ </>
48
+ );
49
+ };
@@ -5,6 +5,9 @@ import { SpeedInsights } from "@vercel/speed-insights/next";
5
5
  import { usePathname } from "next/navigation";
6
6
 
7
7
  const PRIVATE_PATH_PREFIXES = ["/app", "/oauth"];
8
+ const DEFAULT_BLODEMD_GA_ID = "G-WE1RCNSC4E";
9
+ const BLODEMD_GA_ID =
10
+ process.env.NEXT_PUBLIC_BLODEMD_GA_ID?.trim() || DEFAULT_BLODEMD_GA_ID;
8
11
 
9
12
  const isPrivatePath = (pathname: string | null) => {
10
13
  if (!pathname) {
@@ -23,7 +26,7 @@ export const ThirdParties = () => {
23
26
  return (
24
27
  <>
25
28
  <SpeedInsights />
26
- <GoogleAnalytics gaId="G-WE1RCNSC4E" />
29
+ {BLODEMD_GA_ID ? <GoogleAnalytics gaId={BLODEMD_GA_ID} /> : null}
27
30
  </>
28
31
  );
29
32
  };
@@ -6,7 +6,7 @@ import {
6
6
  } from "blode-icons-react";
7
7
  import { cva } from "class-variance-authority";
8
8
  import type { VariantProps } from "class-variance-authority";
9
- import { useCallback } from "react";
9
+ import { useCallback, useEffect, useRef } from "react";
10
10
  import type { MouseEvent } from "react";
11
11
 
12
12
  import { Button as ButtonPrimitive } from "@/components/animate-ui/primitives/buttons/button";
@@ -68,6 +68,16 @@ const CopyButton = ({
68
68
  onChange: onCopiedChange,
69
69
  value: copied,
70
70
  });
71
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
72
+
73
+ useEffect(
74
+ () => () => {
75
+ if (timeoutRef.current) {
76
+ clearTimeout(timeoutRef.current);
77
+ }
78
+ },
79
+ []
80
+ );
71
81
 
72
82
  const handleCopy = useCallback(
73
83
  async (e: MouseEvent<HTMLButtonElement>) => {
@@ -80,7 +90,10 @@ const CopyButton = ({
80
90
  await navigator.clipboard.writeText(content);
81
91
  setIsCopied(true);
82
92
  onCopiedChange?.(true, content);
83
- setTimeout(() => {
93
+ if (timeoutRef.current) {
94
+ clearTimeout(timeoutRef.current);
95
+ }
96
+ timeoutRef.current = setTimeout(() => {
84
97
  setIsCopied(false);
85
98
  onCopiedChange?.(false);
86
99
  }, delay);
@@ -102,6 +115,7 @@ const CopyButton = ({
102
115
  {...props}
103
116
  >
104
117
  <span
118
+ aria-hidden="true"
105
119
  className="inline-flex animate-in fade-in-0 zoom-in-95 duration-200 motion-reduce:animate-none"
106
120
  data-slot="copy-button-icon"
107
121
  key={isCopied ? "check" : "copy"}