blodemd 0.1.0 → 0.2.0

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 (289) hide show
  1. package/dist/cli.mjs +50 -174
  2. package/dist/cli.mjs.map +1 -1
  3. package/package.json +3 -36
  4. package/dev-server/app/[[...slug]]/page.tsx +0 -140
  5. package/dev-server/app/blodemd-dev/invalidate/route.ts +0 -12
  6. package/dev-server/app/blodemd-dev/static/[...path]/route.ts +0 -32
  7. package/dev-server/app/blodemd-dev/version/route.ts +0 -14
  8. package/dev-server/app/blodemd-internal/proxy/route.ts +0 -86
  9. package/dev-server/app/error.tsx +0 -24
  10. package/dev-server/app/favicon.ico +0 -0
  11. package/dev-server/app/globals.css +0 -4
  12. package/dev-server/app/layout.tsx +0 -38
  13. package/dev-server/app/not-found.tsx +0 -18
  14. package/dev-server/app/search/route.ts +0 -17
  15. package/dev-server/components/dev-reload-script.tsx +0 -86
  16. package/dev-server/components/providers.tsx +0 -15
  17. package/dev-server/lib/dev-state.ts +0 -8
  18. package/dev-server/lib/local-content-source.ts +0 -103
  19. package/dev-server/lib/local-runtime.tsx +0 -555
  20. package/dev-server/next-env.d.ts +0 -5
  21. package/dev-server/next.config.js +0 -54
  22. package/dev-server/package.json +0 -61
  23. package/dev-server/postcss.config.mjs +0 -7
  24. package/dev-server/public/glide-variable.woff2 +0 -0
  25. package/dev-server/tsconfig.json +0 -50
  26. package/docs/app/globals.css +0 -471
  27. package/docs/components/animate-ui/primitives/buttons/button.tsx +0 -14
  28. package/docs/components/api/api-playground.tsx +0 -467
  29. package/docs/components/api/api-reference.tsx +0 -157
  30. package/docs/components/content/collection-index.tsx +0 -114
  31. package/docs/components/docs/contextual-menu.tsx +0 -495
  32. package/docs/components/docs/copy-page-menu.tsx +0 -364
  33. package/docs/components/docs/doc-header.tsx +0 -223
  34. package/docs/components/docs/doc-shell.tsx +0 -355
  35. package/docs/components/docs/doc-sidebar.tsx +0 -256
  36. package/docs/components/docs/doc-toc.tsx +0 -46
  37. package/docs/components/docs/mobile-nav.tsx +0 -195
  38. package/docs/components/docs/sidebar-active-highlight.tsx +0 -34
  39. package/docs/components/docs/sidebar-scroll-area.tsx +0 -63
  40. package/docs/components/git/repo-picker.tsx +0 -529
  41. package/docs/components/icons/doc-icon.tsx +0 -96
  42. package/docs/components/mdx/accordion.tsx +0 -83
  43. package/docs/components/mdx/agent-instructions.tsx +0 -17
  44. package/docs/components/mdx/badge.tsx +0 -79
  45. package/docs/components/mdx/callout.tsx +0 -88
  46. package/docs/components/mdx/card.tsx +0 -110
  47. package/docs/components/mdx/code-block.tsx +0 -80
  48. package/docs/components/mdx/code-group.tsx +0 -203
  49. package/docs/components/mdx/color.tsx +0 -87
  50. package/docs/components/mdx/columns.tsx +0 -25
  51. package/docs/components/mdx/expandable.tsx +0 -45
  52. package/docs/components/mdx/field-layout.tsx +0 -77
  53. package/docs/components/mdx/frame.tsx +0 -23
  54. package/docs/components/mdx/get-text-content.ts +0 -18
  55. package/docs/components/mdx/icon.tsx +0 -12
  56. package/docs/components/mdx/iframe.tsx +0 -62
  57. package/docs/components/mdx/index.tsx +0 -121
  58. package/docs/components/mdx/installer.tsx +0 -20
  59. package/docs/components/mdx/panel.tsx +0 -11
  60. package/docs/components/mdx/param-field.tsx +0 -56
  61. package/docs/components/mdx/preview.tsx +0 -36
  62. package/docs/components/mdx/prompt.tsx +0 -63
  63. package/docs/components/mdx/request-example.tsx +0 -27
  64. package/docs/components/mdx/response-field.tsx +0 -42
  65. package/docs/components/mdx/steps.tsx +0 -92
  66. package/docs/components/mdx/tabs.tsx +0 -193
  67. package/docs/components/mdx/tile.tsx +0 -43
  68. package/docs/components/mdx/tooltip.tsx +0 -71
  69. package/docs/components/mdx/tree.tsx +0 -120
  70. package/docs/components/mdx/type-table.tsx +0 -71
  71. package/docs/components/mdx/update.tsx +0 -44
  72. package/docs/components/mdx/video.tsx +0 -45
  73. package/docs/components/mdx/view.tsx +0 -66
  74. package/docs/components/posthog-provider.tsx +0 -42
  75. package/docs/components/providers.tsx +0 -15
  76. package/docs/components/tenant-analytics.tsx +0 -49
  77. package/docs/components/third-parties.tsx +0 -32
  78. package/docs/components/ui/badge.tsx +0 -61
  79. package/docs/components/ui/breadcrumb.tsx +0 -112
  80. package/docs/components/ui/button-group.tsx +0 -83
  81. package/docs/components/ui/button.tsx +0 -65
  82. package/docs/components/ui/card.tsx +0 -92
  83. package/docs/components/ui/command.tsx +0 -167
  84. package/docs/components/ui/copy-button.tsx +0 -129
  85. package/docs/components/ui/dialog.tsx +0 -146
  86. package/docs/components/ui/field.tsx +0 -237
  87. package/docs/components/ui/input.tsx +0 -23
  88. package/docs/components/ui/kbd.tsx +0 -98
  89. package/docs/components/ui/label.tsx +0 -22
  90. package/docs/components/ui/morph-icon.tsx +0 -79
  91. package/docs/components/ui/popover.tsx +0 -267
  92. package/docs/components/ui/search-dialog.tsx +0 -431
  93. package/docs/components/ui/search.tsx +0 -101
  94. package/docs/components/ui/separator.tsx +0 -26
  95. package/docs/components/ui/sheet.tsx +0 -133
  96. package/docs/components/ui/sidebar.tsx +0 -758
  97. package/docs/components/ui/skeleton.tsx +0 -11
  98. package/docs/components/ui/switch.tsx +0 -32
  99. package/docs/components/ui/tabs.tsx +0 -138
  100. package/docs/components/ui/theme-toggle.tsx +0 -62
  101. package/docs/components/ui/tooltip.tsx +0 -53
  102. package/docs/components/web-mcp.tsx +0 -115
  103. package/docs/lib/agent-skills.ts +0 -48
  104. package/docs/lib/api-client.ts +0 -72
  105. package/docs/lib/config.ts +0 -6
  106. package/docs/lib/content-root.ts +0 -33
  107. package/docs/lib/content-source.ts +0 -70
  108. package/docs/lib/contextual-options.ts +0 -222
  109. package/docs/lib/dashboard-session.ts +0 -167
  110. package/docs/lib/db.ts +0 -13
  111. package/docs/lib/docs-collection.ts +0 -7
  112. package/docs/lib/docs-runtime.tsx +0 -745
  113. package/docs/lib/edge-config.ts +0 -95
  114. package/docs/lib/env.ts +0 -23
  115. package/docs/lib/etag.ts +0 -22
  116. package/docs/lib/github-install.ts +0 -33
  117. package/docs/lib/llms-full.ts +0 -82
  118. package/docs/lib/marketing-markdown.ts +0 -79
  119. package/docs/lib/marketing-site.ts +0 -31
  120. package/docs/lib/mdx.ts +0 -98
  121. package/docs/lib/navigation.ts +0 -288
  122. package/docs/lib/openapi-proxy.ts +0 -88
  123. package/docs/lib/openapi.ts +0 -158
  124. package/docs/lib/platform-config.ts +0 -6
  125. package/docs/lib/project-authz.ts +0 -46
  126. package/docs/lib/routes.ts +0 -103
  127. package/docs/lib/server-cache.ts +0 -83
  128. package/docs/lib/shiki.ts +0 -44
  129. package/docs/lib/supabase.ts +0 -37
  130. package/docs/lib/tenancy.ts +0 -364
  131. package/docs/lib/tenant-headers.ts +0 -43
  132. package/docs/lib/tenant-static.ts +0 -705
  133. package/docs/lib/tenant-utility-context.ts +0 -62
  134. package/docs/lib/tenants.ts +0 -72
  135. package/docs/lib/theme.ts +0 -29
  136. package/docs/lib/time-ago.ts +0 -24
  137. package/docs/lib/toc.ts +0 -2
  138. package/docs/lib/use-mobile.ts +0 -19
  139. package/docs/lib/use-tab-observer.ts +0 -71
  140. package/docs/lib/utils.ts +0 -5
  141. package/packages/@repo/common/dist/index.d.ts +0 -16
  142. package/packages/@repo/common/dist/index.d.ts.map +0 -1
  143. package/packages/@repo/common/dist/index.js +0 -84
  144. package/packages/@repo/common/package.json +0 -34
  145. package/packages/@repo/common/src/index.ts +0 -101
  146. package/packages/@repo/contracts/dist/analytics.d.ts +0 -21
  147. package/packages/@repo/contracts/dist/analytics.d.ts.map +0 -1
  148. package/packages/@repo/contracts/dist/analytics.js +0 -18
  149. package/packages/@repo/contracts/dist/api-key.d.ts +0 -27
  150. package/packages/@repo/contracts/dist/api-key.d.ts.map +0 -1
  151. package/packages/@repo/contracts/dist/api-key.js +0 -20
  152. package/packages/@repo/contracts/dist/dates.d.ts +0 -4
  153. package/packages/@repo/contracts/dist/dates.d.ts.map +0 -1
  154. package/packages/@repo/contracts/dist/dates.js +0 -2
  155. package/packages/@repo/contracts/dist/deployment.d.ts +0 -71
  156. package/packages/@repo/contracts/dist/deployment.d.ts.map +0 -1
  157. package/packages/@repo/contracts/dist/deployment.js +0 -46
  158. package/packages/@repo/contracts/dist/domain.d.ts +0 -94
  159. package/packages/@repo/contracts/dist/domain.d.ts.map +0 -1
  160. package/packages/@repo/contracts/dist/domain.js +0 -36
  161. package/packages/@repo/contracts/dist/git.d.ts +0 -28
  162. package/packages/@repo/contracts/dist/git.d.ts.map +0 -1
  163. package/packages/@repo/contracts/dist/git.js +0 -24
  164. package/packages/@repo/contracts/dist/ids.d.ts +0 -14
  165. package/packages/@repo/contracts/dist/ids.d.ts.map +0 -1
  166. package/packages/@repo/contracts/dist/ids.js +0 -11
  167. package/packages/@repo/contracts/dist/index.d.ts +0 -12
  168. package/packages/@repo/contracts/dist/index.d.ts.map +0 -1
  169. package/packages/@repo/contracts/dist/index.js +0 -13
  170. package/packages/@repo/contracts/dist/pagination.d.ts +0 -23
  171. package/packages/@repo/contracts/dist/pagination.d.ts.map +0 -1
  172. package/packages/@repo/contracts/dist/pagination.js +0 -15
  173. package/packages/@repo/contracts/dist/project.d.ts +0 -42
  174. package/packages/@repo/contracts/dist/project.d.ts.map +0 -1
  175. package/packages/@repo/contracts/dist/project.js +0 -25
  176. package/packages/@repo/contracts/dist/tenant.d.ts +0 -147
  177. package/packages/@repo/contracts/dist/tenant.d.ts.map +0 -1
  178. package/packages/@repo/contracts/dist/tenant.js +0 -58
  179. package/packages/@repo/contracts/dist/user.d.ts +0 -9
  180. package/packages/@repo/contracts/dist/user.d.ts.map +0 -1
  181. package/packages/@repo/contracts/dist/user.js +0 -9
  182. package/packages/@repo/contracts/package.json +0 -37
  183. package/packages/@repo/contracts/src/analytics.ts +0 -33
  184. package/packages/@repo/contracts/src/api-key.ts +0 -27
  185. package/packages/@repo/contracts/src/dates.ts +0 -4
  186. package/packages/@repo/contracts/src/deployment.ts +0 -73
  187. package/packages/@repo/contracts/src/domain.ts +0 -51
  188. package/packages/@repo/contracts/src/git.ts +0 -31
  189. package/packages/@repo/contracts/src/ids.ts +0 -23
  190. package/packages/@repo/contracts/src/index.ts +0 -13
  191. package/packages/@repo/contracts/src/pagination.ts +0 -21
  192. package/packages/@repo/contracts/src/project.ts +0 -32
  193. package/packages/@repo/contracts/src/tenant.ts +0 -94
  194. package/packages/@repo/contracts/src/user.ts +0 -12
  195. package/packages/@repo/models/dist/docs-config.d.ts +0 -961
  196. package/packages/@repo/models/dist/docs-config.d.ts.map +0 -1
  197. package/packages/@repo/models/dist/docs-config.js +0 -531
  198. package/packages/@repo/models/dist/index.d.ts +0 -3
  199. package/packages/@repo/models/dist/index.d.ts.map +0 -1
  200. package/packages/@repo/models/dist/index.js +0 -3
  201. package/packages/@repo/models/dist/tenant.d.ts +0 -37
  202. package/packages/@repo/models/dist/tenant.d.ts.map +0 -1
  203. package/packages/@repo/models/dist/tenant.js +0 -1
  204. package/packages/@repo/models/package.json +0 -37
  205. package/packages/@repo/models/src/docs-config.ts +0 -629
  206. package/packages/@repo/models/src/index.ts +0 -3
  207. package/packages/@repo/models/src/tenant.ts +0 -44
  208. package/packages/@repo/prebuild/dist/index.d.ts +0 -2
  209. package/packages/@repo/prebuild/dist/index.d.ts.map +0 -1
  210. package/packages/@repo/prebuild/dist/index.js +0 -2
  211. package/packages/@repo/prebuild/dist/openapi.d.ts +0 -43
  212. package/packages/@repo/prebuild/dist/openapi.d.ts.map +0 -1
  213. package/packages/@repo/prebuild/dist/openapi.js +0 -86
  214. package/packages/@repo/prebuild/package.json +0 -39
  215. package/packages/@repo/prebuild/src/index.ts +0 -2
  216. package/packages/@repo/prebuild/src/openapi.ts +0 -154
  217. package/packages/@repo/previewing/dist/blob-source.d.ts +0 -16
  218. package/packages/@repo/previewing/dist/blob-source.d.ts.map +0 -1
  219. package/packages/@repo/previewing/dist/blob-source.js +0 -128
  220. package/packages/@repo/previewing/dist/config-loader.d.ts +0 -134
  221. package/packages/@repo/previewing/dist/config-loader.d.ts.map +0 -1
  222. package/packages/@repo/previewing/dist/config-loader.js +0 -231
  223. package/packages/@repo/previewing/dist/constants.d.ts +0 -15
  224. package/packages/@repo/previewing/dist/constants.d.ts.map +0 -1
  225. package/packages/@repo/previewing/dist/constants.js +0 -14
  226. package/packages/@repo/previewing/dist/content-index.d.ts +0 -7
  227. package/packages/@repo/previewing/dist/content-index.d.ts.map +0 -1
  228. package/packages/@repo/previewing/dist/content-index.js +0 -291
  229. package/packages/@repo/previewing/dist/content-source.d.ts +0 -12
  230. package/packages/@repo/previewing/dist/content-source.d.ts.map +0 -1
  231. package/packages/@repo/previewing/dist/content-source.js +0 -1
  232. package/packages/@repo/previewing/dist/fs-source.d.ts +0 -11
  233. package/packages/@repo/previewing/dist/fs-source.d.ts.map +0 -1
  234. package/packages/@repo/previewing/dist/fs-source.js +0 -71
  235. package/packages/@repo/previewing/dist/index.d.ts +0 -16
  236. package/packages/@repo/previewing/dist/index.d.ts.map +0 -1
  237. package/packages/@repo/previewing/dist/index.js +0 -13
  238. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +0 -3
  239. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +0 -1
  240. package/packages/@repo/previewing/dist/markdown/agent-markdown.js +0 -176
  241. package/packages/@repo/previewing/dist/markdown/format.d.ts +0 -8
  242. package/packages/@repo/previewing/dist/markdown/format.d.ts.map +0 -1
  243. package/packages/@repo/previewing/dist/markdown/format.js +0 -43
  244. package/packages/@repo/previewing/dist/markdown/links.d.ts +0 -3
  245. package/packages/@repo/previewing/dist/markdown/links.d.ts.map +0 -1
  246. package/packages/@repo/previewing/dist/markdown/links.js +0 -31
  247. package/packages/@repo/previewing/dist/openapi-pages.d.ts +0 -7
  248. package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +0 -1
  249. package/packages/@repo/previewing/dist/openapi-pages.js +0 -200
  250. package/packages/@repo/previewing/dist/search-index.d.ts +0 -4
  251. package/packages/@repo/previewing/dist/search-index.d.ts.map +0 -1
  252. package/packages/@repo/previewing/dist/search-index.js +0 -23
  253. package/packages/@repo/previewing/dist/serialization.d.ts +0 -13
  254. package/packages/@repo/previewing/dist/serialization.d.ts.map +0 -1
  255. package/packages/@repo/previewing/dist/serialization.js +0 -108
  256. package/packages/@repo/previewing/dist/toc-index.d.ts +0 -5
  257. package/packages/@repo/previewing/dist/toc-index.d.ts.map +0 -1
  258. package/packages/@repo/previewing/dist/toc-index.js +0 -53
  259. package/packages/@repo/previewing/dist/types.d.ts +0 -92
  260. package/packages/@repo/previewing/dist/types.d.ts.map +0 -1
  261. package/packages/@repo/previewing/dist/types.js +0 -1
  262. package/packages/@repo/previewing/dist/utility-index.d.ts +0 -7
  263. package/packages/@repo/previewing/dist/utility-index.d.ts.map +0 -1
  264. package/packages/@repo/previewing/dist/utility-index.js +0 -201
  265. package/packages/@repo/previewing/package.json +0 -41
  266. package/packages/@repo/previewing/src/blob-source.ts +0 -185
  267. package/packages/@repo/previewing/src/config-loader.ts +0 -316
  268. package/packages/@repo/previewing/src/constants.ts +0 -16
  269. package/packages/@repo/previewing/src/content-index.ts +0 -384
  270. package/packages/@repo/previewing/src/content-source.ts +0 -12
  271. package/packages/@repo/previewing/src/fs-source.ts +0 -103
  272. package/packages/@repo/previewing/src/index.ts +0 -71
  273. package/packages/@repo/previewing/src/markdown/agent-markdown.ts +0 -254
  274. package/packages/@repo/previewing/src/markdown/format.ts +0 -63
  275. package/packages/@repo/previewing/src/markdown/links.ts +0 -46
  276. package/packages/@repo/previewing/src/openapi-pages.ts +0 -319
  277. package/packages/@repo/previewing/src/search-index.ts +0 -38
  278. package/packages/@repo/previewing/src/serialization.ts +0 -191
  279. package/packages/@repo/previewing/src/toc-index.ts +0 -72
  280. package/packages/@repo/previewing/src/types.ts +0 -105
  281. package/packages/@repo/previewing/src/utility-index.ts +0 -304
  282. package/packages/@repo/validation/dist/index.d.ts +0 -12
  283. package/packages/@repo/validation/dist/index.d.ts.map +0 -1
  284. package/packages/@repo/validation/dist/index.js +0 -30
  285. package/packages/@repo/validation/package.json +0 -38
  286. package/packages/@repo/validation/src/blodemd-docs-schema.json +0 -3575
  287. package/packages/@repo/validation/src/index.ts +0 -59
  288. package/scripts/prepare-package.mjs +0 -53
  289. package/scripts/repo-packages.mjs +0 -8
@@ -1,114 +0,0 @@
1
- import type { ContentEntry } from "@repo/previewing";
2
- import Link from "next/link";
3
-
4
- import { toDocHref } from "@/lib/routes";
5
-
6
- type CollectionEntry = Extract<ContentEntry, { kind: "entry" }>;
7
-
8
- const formatDate = (value?: string) => {
9
- if (!value) {
10
- return null;
11
- }
12
- const date = new Date(value);
13
- if (Number.isNaN(date.getTime())) {
14
- return value;
15
- }
16
- return date.toLocaleDateString("en-US", {
17
- day: "numeric",
18
- month: "short",
19
- year: "numeric",
20
- });
21
- };
22
-
23
- const formatPrice = (price?: number, currency?: string) => {
24
- if (price === undefined || !currency) {
25
- return null;
26
- }
27
- try {
28
- return new Intl.NumberFormat("en-US", {
29
- currency,
30
- style: "currency",
31
- }).format(price);
32
- } catch {
33
- return `${price} ${currency}`;
34
- }
35
- };
36
-
37
- const buildMeta = (entry: CollectionEntry) => {
38
- const frontmatter = entry.frontmatter as Record<string, unknown>;
39
- switch (entry.type) {
40
- case "blog": {
41
- const date = formatDate(frontmatter.date as string | undefined);
42
- const tags = Array.isArray(frontmatter.tags)
43
- ? frontmatter.tags.join(", ")
44
- : null;
45
- return [date, tags ? `Tags: ${tags}` : null].filter(Boolean);
46
- }
47
- case "courses": {
48
- const order = frontmatter.order as number | undefined;
49
- return order === undefined ? [] : [`Lesson ${order}`];
50
- }
51
- case "products": {
52
- const sku = frontmatter.sku as string | undefined;
53
- const price = formatPrice(
54
- frontmatter.price as number | undefined,
55
- frontmatter.currency as string | undefined
56
- );
57
- return [sku ? `SKU ${sku}` : null, price].filter(Boolean);
58
- }
59
- case "notes":
60
- case "todos": {
61
- const date = formatDate(frontmatter.date as string | undefined);
62
- return date ? [date] : [];
63
- }
64
- case "forms": {
65
- const fields = Array.isArray(frontmatter.fields)
66
- ? frontmatter.fields.length
67
- : null;
68
- return fields ? [`${fields} fields`] : [];
69
- }
70
- case "sheets": {
71
- const columns = Array.isArray(frontmatter.columns)
72
- ? frontmatter.columns.length
73
- : null;
74
- return columns ? [`${columns} columns`] : [];
75
- }
76
- default: {
77
- return [];
78
- }
79
- }
80
- };
81
-
82
- export const CollectionIndex = ({
83
- entries,
84
- basePath,
85
- }: {
86
- entries: CollectionEntry[];
87
- basePath: string;
88
- }) => (
89
- <div className="grid gap-4">
90
- {entries.map((entry) => {
91
- const meta = buildMeta(entry);
92
- return (
93
- <Link
94
- className="block rounded-xl border border-border bg-surface p-4.5 transition-[border-color,box-shadow] hover:border-primary hover:shadow-md"
95
- href={toDocHref(entry.slug, basePath)}
96
- key={`${entry.collectionId}-${entry.slug}`}
97
- >
98
- <div className="text-lg font-semibold">{entry.title}</div>
99
- {entry.description ? (
100
- <p className="mt-1.5 text-muted-foreground">{entry.description}</p>
101
- ) : null}
102
- {meta.length ? (
103
- <div className="mt-2 text-sm text-muted-foreground">
104
- {meta.join(" | ")}
105
- </div>
106
- ) : null}
107
- {entry.type === "slides" ? (
108
- <div className="mt-2 text-sm text-muted-foreground">Slide deck</div>
109
- ) : null}
110
- </Link>
111
- );
112
- })}
113
- </div>
114
- );
@@ -1,495 +0,0 @@
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
- Popover,
27
- PopoverContent,
28
- PopoverTrigger,
29
- } from "@/components/ui/popover";
30
- import {
31
- buildBuiltinUrl,
32
- builtinOptions,
33
- resolveCustomHref,
34
- } from "@/lib/contextual-options";
35
-
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
- }
60
-
61
- const iconMap: Record<string, IconComponent> = {
62
- ClaudeaiIcon,
63
- CodeAssistantIcon,
64
- CodeBracketsIcon,
65
- CodeIcon,
66
- CodeLinesIcon,
67
- CopySimpleIcon,
68
- GoogleColoredIcon,
69
- GrokIcon,
70
- MarkdownIcon,
71
- OpenaiIcon,
72
- PerplexityIcon,
73
- Plugin1Icon,
74
- SparkleIcon,
75
- WindIcon,
76
- };
77
-
78
- const getBuiltinIcon = (iconName: string): IconComponent =>
79
- iconMap[iconName] ?? CopySimpleIcon;
80
-
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
- };
97
-
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
- };
114
-
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
- };
125
-
126
- const resolveOptions = (
127
- options: ContextualOption[],
128
- context: ContextualContext
129
- ): ResolvedOption[] => {
130
- const resolved: ResolvedOption[] = [];
131
-
132
- for (const option of options) {
133
- if (typeof option === "string") {
134
- const definition = builtinOptions[option];
135
- if (!definition) {
136
- continue;
137
- }
138
-
139
- if (definition.type === "action") {
140
- resolved.push({
141
- action: option as ActionId,
142
- description: definition.description,
143
- icon: getBuiltinIcon(definition.iconName),
144
- key: option,
145
- title: definition.title,
146
- type: "action",
147
- });
148
- continue;
149
- }
150
-
151
- const href = buildBuiltinUrl(option, context);
152
- if (!href) {
153
- continue;
154
- }
155
-
156
- resolved.push({
157
- description: definition.description,
158
- href,
159
- icon: getBuiltinIcon(definition.iconName),
160
- key: option,
161
- title: definition.title,
162
- type: "link",
163
- });
164
- continue;
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
- });
175
- }
176
-
177
- return resolved;
178
- };
179
-
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
- );
207
-
208
- const handleAction = useCallback(
209
- async (action: string, key?: string) => {
210
- const id = key ?? action;
211
-
212
- switch (action) {
213
- case "copy": {
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
- }
224
- }
225
- default: {
226
- return true;
227
- }
228
- }
229
- },
230
- [content, setActionFeedback, title]
231
- );
232
-
233
- return { feedback, handleAction };
234
- };
235
-
236
- const usePageContext = (
237
- content: string | undefined,
238
- title: string,
239
- pagePath: string
240
- ): ContextualContext => {
241
- const [pageUrl, setPageUrl] = useState("");
242
-
243
- useEffect(() => {
244
- setPageUrl(window.location.href);
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
- );
256
- };
257
-
258
- const MenuIcon = ({ children }: { children: ReactNode }) => (
259
- <div className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border">
260
- {children}
261
- </div>
262
- );
263
-
264
- const MenuItem = ({
265
- children,
266
- href,
267
- onSelect,
268
- }: {
269
- children: ReactNode;
270
- href?: string;
271
- onSelect?: () => Promise<void> | void;
272
- }) =>
273
- href ? (
274
- <a
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"
276
- href={href}
277
- onClick={onSelect}
278
- rel="noopener noreferrer"
279
- target="_blank"
280
- >
281
- {children}
282
- </a>
283
- ) : (
284
- <button
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"
286
- onClick={onSelect}
287
- type="button"
288
- >
289
- {children}
290
- </button>
291
- );
292
-
293
- const ExternalArrow = () => (
294
- <svg
295
- aria-hidden="true"
296
- className="ml-1 inline-block size-3"
297
- fill="none"
298
- stroke="currentColor"
299
- strokeLinecap="round"
300
- strokeLinejoin="round"
301
- strokeWidth={2}
302
- viewBox="0 0 24 24"
303
- >
304
- <path d="M7 17L17 7M7 7h10v10" />
305
- </svg>
306
- );
307
-
308
- interface ContextualMenuProps {
309
- options: ContextualOption[];
310
- content?: string;
311
- title: string;
312
- pagePath: string;
313
- }
314
-
315
- export const ContextualMenu = ({
316
- options,
317
- content,
318
- title,
319
- pagePath,
320
- }: ContextualMenuProps) => {
321
- const context = usePageContext(content, title, pagePath);
322
- const { feedback, handleAction } = useContextualActions(content, title);
323
- const [menuOpen, setMenuOpen] = useState(false);
324
-
325
- const resolved = useMemo(
326
- () => resolveOptions(options, context),
327
- [context, options]
328
- );
329
- const [primaryOption] = resolved;
330
- const hasSecondaryOptions = resolved.length > 1;
331
-
332
- const closeMenu = useCallback(() => {
333
- setMenuOpen(false);
334
- }, []);
335
-
336
- const handlePrimaryAction = useCallback(async () => {
337
- if (!primaryOption) {
338
- return;
339
- }
340
-
341
- if (primaryOption.type === "link" && primaryOption.href) {
342
- window.open(primaryOption.href, "_blank", "noopener,noreferrer");
343
- return;
344
- }
345
-
346
- await handleAction(primaryOption.action ?? "", primaryOption.key);
347
- }, [handleAction, primaryOption]);
348
-
349
- const actionHandlers = useMemo(
350
- () =>
351
- Object.fromEntries(
352
- resolved
353
- .filter((item) => item.type === "action")
354
- .map((item) => [
355
- item.key,
356
- async () => {
357
- const didComplete = await handleAction(
358
- item.action ?? "",
359
- item.key
360
- );
361
- if (didComplete) {
362
- closeMenu();
363
- }
364
- },
365
- ])
366
- ) as Record<string, () => Promise<void>>,
367
- [closeMenu, handleAction, resolved]
368
- );
369
-
370
- if (!primaryOption) {
371
- return null;
372
- }
373
-
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
- }
395
-
396
- return (
397
- <Popover onOpenChange={setMenuOpen} open={menuOpen}>
398
- <div className="flex shrink-0 items-center">
399
- {primaryButton}
400
-
401
- <PopoverTrigger asChild>
402
- <button
403
- aria-expanded={menuOpen}
404
- aria-label="More actions"
405
- className="inline-flex items-center self-stretch rounded-r-xl border border-border px-2 transition-colors hover:bg-secondary/25"
406
- type="button"
407
- >
408
- <ChevronDownSmallIcon
409
- aria-hidden="true"
410
- className="size-[18px] text-muted-foreground"
411
- />
412
- </button>
413
- </PopoverTrigger>
414
- </div>
415
-
416
- <PopoverContent align="end" className="w-[320px] rounded-xl p-1">
417
- {menuOpen
418
- ? resolved.slice(1).map((item) => {
419
- const itemFeedback =
420
- feedback?.id === item.key ? feedback.state : null;
421
- const itemLabel = getFeedbackLabel(itemFeedback, item.title);
422
- const itemDescription = getFeedbackDescription(
423
- itemFeedback,
424
- item.description
425
- );
426
- const ItemIcon = getFeedbackIcon(itemFeedback, item.icon);
427
-
428
- return (
429
- <MenuItem
430
- href={item.type === "link" ? item.href : undefined}
431
- key={item.key}
432
- onSelect={
433
- item.type === "action"
434
- ? actionHandlers[item.key]
435
- : closeMenu
436
- }
437
- >
438
- <MenuIcon>
439
- <ItemIcon aria-hidden="true" className="size-[18px]" />
440
- </MenuIcon>
441
- <div className="flex-1">
442
- <div className="font-medium">
443
- {itemLabel}
444
- {item.type === "link" ? <ExternalArrow /> : null}
445
- </div>
446
- <div className="text-xs text-muted-foreground">
447
- {itemDescription}
448
- </div>
449
- </div>
450
- </MenuItem>
451
- );
452
- })
453
- : null}
454
- </PopoverContent>
455
- </Popover>
456
- );
457
- };
458
-
459
- export const ContextualTocItems = ({
460
- content,
461
- options,
462
- title,
463
- pagePath,
464
- }: ContextualMenuProps) => {
465
- const context = usePageContext(content, title, pagePath);
466
- const resolved = useMemo(
467
- () =>
468
- resolveOptions(options, context).filter(
469
- (item): item is ResolvedOption & { href: string; type: "link" } =>
470
- item.type === "link" && Boolean(item.href)
471
- ),
472
- [context, options]
473
- );
474
-
475
- if (!resolved.length) {
476
- return null;
477
- }
478
-
479
- return (
480
- <div className="grid gap-1.5">
481
- {resolved.map((item) => (
482
- <a
483
- 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"
484
- href={item.href}
485
- key={item.key}
486
- rel="noopener noreferrer"
487
- target="_blank"
488
- >
489
- <item.icon aria-hidden="true" className="size-3.5 shrink-0" />
490
- <span className="truncate">{item.title}</span>
491
- </a>
492
- ))}
493
- </div>
494
- );
495
- };