@suzumiyaaoba/mdxr 0.1.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 (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +2902 -0
  5. package/dist/components.d.mts +791 -0
  6. package/dist/components.mjs +2 -0
  7. package/dist/config-SI9IyFiC.mjs +184 -0
  8. package/dist/doc-context-CEqzMYKv.d.mts +568 -0
  9. package/dist/index.d.mts +20 -0
  10. package/dist/index.mjs +4 -0
  11. package/dist/ui-GKFD0mx5.mjs +15986 -0
  12. package/package.json +131 -0
  13. package/skill/SKILL.md +49 -0
  14. package/skill/references/components/charts.md +172 -0
  15. package/skill/references/components/document.md +98 -0
  16. package/skill/references/components/forms.md +35 -0
  17. package/skill/references/components/investigation.md +198 -0
  18. package/skill/references/components/layout.md +68 -0
  19. package/skill/references/components/output.md +158 -0
  20. package/skill/references/components/planning.md +155 -0
  21. package/skill/references/components/reports.md +252 -0
  22. package/skill/references/components/shadcn.md +19 -0
  23. package/skill/references/components.md +186 -0
  24. package/skill/references/extending.md +48 -0
  25. package/src/ask-sheet.ts +40 -0
  26. package/src/assets/css.ts +392 -0
  27. package/src/assets/scripts.ts +78 -0
  28. package/src/catalog.ts +287 -0
  29. package/src/cli.ts +178 -0
  30. package/src/client/doc-events.ts +1108 -0
  31. package/src/client/entry.ts +27 -0
  32. package/src/client-js.ts +47 -0
  33. package/src/component-map.ts +47 -0
  34. package/src/components/ui/accordion.tsx +77 -0
  35. package/src/components/ui/alert-dialog.tsx +185 -0
  36. package/src/components/ui/alert.tsx +76 -0
  37. package/src/components/ui/aspect-ratio.tsx +22 -0
  38. package/src/components/ui/attachment.tsx +208 -0
  39. package/src/components/ui/avatar.tsx +106 -0
  40. package/src/components/ui/badge.tsx +52 -0
  41. package/src/components/ui/breadcrumb.tsx +121 -0
  42. package/src/components/ui/bubble.tsx +128 -0
  43. package/src/components/ui/button-group.tsx +88 -0
  44. package/src/components/ui/button.tsx +58 -0
  45. package/src/components/ui/calendar.tsx +226 -0
  46. package/src/components/ui/card.tsx +102 -0
  47. package/src/components/ui/carousel.tsx +246 -0
  48. package/src/components/ui/chart.tsx +379 -0
  49. package/src/components/ui/checkbox.tsx +27 -0
  50. package/src/components/ui/collapsible.tsx +19 -0
  51. package/src/components/ui/combobox.tsx +298 -0
  52. package/src/components/ui/command.tsx +193 -0
  53. package/src/components/ui/context-menu.tsx +271 -0
  54. package/src/components/ui/dialog.tsx +159 -0
  55. package/src/components/ui/direction.tsx +4 -0
  56. package/src/components/ui/drawer.tsx +227 -0
  57. package/src/components/ui/dropdown-menu.tsx +269 -0
  58. package/src/components/ui/empty.tsx +104 -0
  59. package/src/components/ui/field.tsx +237 -0
  60. package/src/components/ui/fieldset.tsx +32 -0
  61. package/src/components/ui/frame.tsx +87 -0
  62. package/src/components/ui/hover-card.tsx +50 -0
  63. package/src/components/ui/input-group.tsx +159 -0
  64. package/src/components/ui/input-otp.tsx +83 -0
  65. package/src/components/ui/input.tsx +19 -0
  66. package/src/components/ui/item.tsx +202 -0
  67. package/src/components/ui/kbd.tsx +26 -0
  68. package/src/components/ui/label.tsx +19 -0
  69. package/src/components/ui/marker.tsx +71 -0
  70. package/src/components/ui/menubar.tsx +284 -0
  71. package/src/components/ui/message-scroller.tsx +128 -0
  72. package/src/components/ui/message.tsx +91 -0
  73. package/src/components/ui/meter.tsx +80 -0
  74. package/src/components/ui/native-select.tsx +64 -0
  75. package/src/components/ui/navigation-menu.tsx +170 -0
  76. package/src/components/ui/pagination.tsx +133 -0
  77. package/src/components/ui/popover.tsx +87 -0
  78. package/src/components/ui/progress.tsx +82 -0
  79. package/src/components/ui/questionnaire.tsx +328 -0
  80. package/src/components/ui/radio-group.tsx +35 -0
  81. package/src/components/ui/resizable.tsx +49 -0
  82. package/src/components/ui/scroll-area.tsx +50 -0
  83. package/src/components/ui/select.tsx +201 -0
  84. package/src/components/ui/separator.tsx +22 -0
  85. package/src/components/ui/sheet.tsx +135 -0
  86. package/src/components/ui/sidebar.tsx +730 -0
  87. package/src/components/ui/skeleton.tsx +13 -0
  88. package/src/components/ui/slider.tsx +51 -0
  89. package/src/components/ui/spinner.tsx +16 -0
  90. package/src/components/ui/switch.tsx +31 -0
  91. package/src/components/ui/table.tsx +113 -0
  92. package/src/components/ui/tabs.tsx +82 -0
  93. package/src/components/ui/textarea.tsx +17 -0
  94. package/src/components/ui/toast.tsx +229 -0
  95. package/src/components/ui/toggle-group.tsx +87 -0
  96. package/src/components/ui/toggle.tsx +43 -0
  97. package/src/components/ui/tooltip.tsx +65 -0
  98. package/src/components.ts +78 -0
  99. package/src/config.ts +56 -0
  100. package/src/define.ts +122 -0
  101. package/src/doc-context.ts +20 -0
  102. package/src/editor.ts +94 -0
  103. package/src/format-error.ts +78 -0
  104. package/src/guards.ts +84 -0
  105. package/src/hooks/use-mobile.ts +21 -0
  106. package/src/html.ts +91 -0
  107. package/src/hydrate/export-index.ts +232 -0
  108. package/src/hydrate/import-scan.ts +169 -0
  109. package/src/hydrate/plugins.ts +118 -0
  110. package/src/hydrate/runtime-module.ts +145 -0
  111. package/src/hydrate-runtime.ts +67 -0
  112. package/src/hydrate.ts +147 -0
  113. package/src/index.ts +16 -0
  114. package/src/init.ts +57 -0
  115. package/src/langs.ts +148 -0
  116. package/src/lines.ts +53 -0
  117. package/src/load-user-module.ts +191 -0
  118. package/src/mdx.ts +248 -0
  119. package/src/paths.ts +36 -0
  120. package/src/rehype/shiki.ts +533 -0
  121. package/src/remark/alerts.ts +53 -0
  122. package/src/remark/ast.ts +96 -0
  123. package/src/remark/callouts.ts +25 -0
  124. package/src/remark/code-file.ts +85 -0
  125. package/src/remark/code-meta.ts +21 -0
  126. package/src/remark/directives.ts +170 -0
  127. package/src/remark/file-paths.ts +64 -0
  128. package/src/remark/headings.ts +131 -0
  129. package/src/remark/no-js.ts +40 -0
  130. package/src/render.ts +337 -0
  131. package/src/serve.ts +322 -0
  132. package/src/styles/globals.css +134 -0
  133. package/src/styles/shadcn.css +641 -0
  134. package/src/tailwind.ts +119 -0
  135. package/src/ui/approvals.tsx +76 -0
  136. package/src/ui/ask-question.tsx +386 -0
  137. package/src/ui/ask.tsx +206 -0
  138. package/src/ui/attrs.ts +51 -0
  139. package/src/ui/audit.tsx +139 -0
  140. package/src/ui/bar-chart.tsx +334 -0
  141. package/src/ui/benchmarks.tsx +143 -0
  142. package/src/ui/bits.tsx +537 -0
  143. package/src/ui/board.tsx +173 -0
  144. package/src/ui/bridge.tsx +207 -0
  145. package/src/ui/bumps.tsx +178 -0
  146. package/src/ui/callout.tsx +106 -0
  147. package/src/ui/changes.tsx +89 -0
  148. package/src/ui/chart-bits.tsx +52 -0
  149. package/src/ui/chart.ts +577 -0
  150. package/src/ui/checks.tsx +203 -0
  151. package/src/ui/child-index.tsx +44 -0
  152. package/src/ui/children.ts +43 -0
  153. package/src/ui/chips.tsx +49 -0
  154. package/src/ui/cmd.tsx +27 -0
  155. package/src/ui/columns.tsx +31 -0
  156. package/src/ui/comments.tsx +544 -0
  157. package/src/ui/compare.tsx +67 -0
  158. package/src/ui/decision.tsx +79 -0
  159. package/src/ui/deps.tsx +78 -0
  160. package/src/ui/details.tsx +43 -0
  161. package/src/ui/diff-parse.ts +307 -0
  162. package/src/ui/diff.tsx +458 -0
  163. package/src/ui/diffstat.tsx +58 -0
  164. package/src/ui/due.tsx +65 -0
  165. package/src/ui/effort.tsx +29 -0
  166. package/src/ui/endpoints.tsx +118 -0
  167. package/src/ui/envvars.tsx +96 -0
  168. package/src/ui/figure.tsx +37 -0
  169. package/src/ui/file-icon.ts +1020 -0
  170. package/src/ui/file-link.ts +32 -0
  171. package/src/ui/file-ref.tsx +39 -0
  172. package/src/ui/files.tsx +112 -0
  173. package/src/ui/findings.tsx +85 -0
  174. package/src/ui/flow.tsx +73 -0
  175. package/src/ui/funnel.tsx +121 -0
  176. package/src/ui/gantt.tsx +443 -0
  177. package/src/ui/gauges.tsx +135 -0
  178. package/src/ui/glossary.tsx +29 -0
  179. package/src/ui/graph-layout.ts +149 -0
  180. package/src/ui/graph-specs.tsx +102 -0
  181. package/src/ui/graph.tsx +278 -0
  182. package/src/ui/grid.tsx +119 -0
  183. package/src/ui/hypothesis.tsx +94 -0
  184. package/src/ui/icon.tsx +80 -0
  185. package/src/ui/incident.tsx +130 -0
  186. package/src/ui/index.ts +342 -0
  187. package/src/ui/ins-del.tsx +41 -0
  188. package/src/ui/json.tsx +190 -0
  189. package/src/ui/layout.ts +9 -0
  190. package/src/ui/line-chart.tsx +251 -0
  191. package/src/ui/matrix.tsx +208 -0
  192. package/src/ui/meta.tsx +73 -0
  193. package/src/ui/option.tsx +64 -0
  194. package/src/ui/owner.tsx +40 -0
  195. package/src/ui/packages.tsx +104 -0
  196. package/src/ui/pathway.tsx +96 -0
  197. package/src/ui/phase.tsx +41 -0
  198. package/src/ui/pie-chart.tsx +170 -0
  199. package/src/ui/plan.tsx +77 -0
  200. package/src/ui/pre.tsx +175 -0
  201. package/src/ui/priority.tsx +51 -0
  202. package/src/ui/props.tsx +77 -0
  203. package/src/ui/quadrant.tsx +172 -0
  204. package/src/ui/radar.tsx +200 -0
  205. package/src/ui/ref.tsx +160 -0
  206. package/src/ui/release.tsx +178 -0
  207. package/src/ui/req.tsx +42 -0
  208. package/src/ui/review.tsx +133 -0
  209. package/src/ui/risk.tsx +67 -0
  210. package/src/ui/sankey.tsx +287 -0
  211. package/src/ui/scatter.tsx +237 -0
  212. package/src/ui/schema.tsx +113 -0
  213. package/src/ui/score.tsx +105 -0
  214. package/src/ui/search.tsx +120 -0
  215. package/src/ui/series.tsx +38 -0
  216. package/src/ui/severity.tsx +69 -0
  217. package/src/ui/shadcn.tsx +216 -0
  218. package/src/ui/spark.tsx +86 -0
  219. package/src/ui/stack.tsx +57 -0
  220. package/src/ui/stats.tsx +63 -0
  221. package/src/ui/status-badge.tsx +66 -0
  222. package/src/ui/statuspage.tsx +238 -0
  223. package/src/ui/steps.tsx +74 -0
  224. package/src/ui/summary.tsx +41 -0
  225. package/src/ui/symbol-ref.tsx +73 -0
  226. package/src/ui/terminal.tsx +117 -0
  227. package/src/ui/tests.tsx +218 -0
  228. package/src/ui/timeline.tsx +63 -0
  229. package/src/ui/toc.tsx +56 -0
  230. package/src/ui/tones.ts +187 -0
  231. package/src/ui/trace.tsx +69 -0
  232. package/src/ui/tree.tsx +281 -0
  233. package/src/ui/treemap.tsx +128 -0
  234. package/src/ui/venn.tsx +258 -0
  235. package/src/ui/verdict.tsx +78 -0
  236. package/src/ui/waterfall.tsx +142 -0
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
4
+ import { cn } from "cn";
5
+
6
+ function TooltipProvider({
7
+ delay = 0,
8
+ ...props
9
+ }: TooltipPrimitive.Provider.Props) {
10
+ return (
11
+ <TooltipPrimitive.Provider
12
+ data-slot="tooltip-provider"
13
+ delay={delay}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
20
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
21
+ }
22
+
23
+ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
24
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
25
+ }
26
+
27
+ function TooltipContent({
28
+ className,
29
+ side = "top",
30
+ sideOffset = 4,
31
+ align = "center",
32
+ alignOffset = 0,
33
+ children,
34
+ ...props
35
+ }: TooltipPrimitive.Popup.Props &
36
+ Pick<
37
+ TooltipPrimitive.Positioner.Props,
38
+ "align" | "alignOffset" | "side" | "sideOffset"
39
+ >) {
40
+ return (
41
+ <TooltipPrimitive.Portal>
42
+ <TooltipPrimitive.Positioner
43
+ align={align}
44
+ alignOffset={alignOffset}
45
+ side={side}
46
+ sideOffset={sideOffset}
47
+ className="isolate z-50"
48
+ >
49
+ <TooltipPrimitive.Popup
50
+ data-slot="tooltip-content"
51
+ className={cn(
52
+ "bg-foreground text-background data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm",
53
+ className
54
+ )}
55
+ {...props}
56
+ >
57
+ {children}
58
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
59
+ </TooltipPrimitive.Popup>
60
+ </TooltipPrimitive.Positioner>
61
+ </TooltipPrimitive.Portal>
62
+ );
63
+ }
64
+
65
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -0,0 +1,78 @@
1
+ // Public entry: `@suzumiyaaoba/mdxr/components`
2
+ // Re-export the built-in catalog so custom components can compose them.
3
+ // `export *` keeps the surface complete — the hydration bundle never loads
4
+ // this barrel (it resolves each used name straight to its leaf module).
5
+ export * from "./ui/index.js";
6
+ export { DocContext } from "./doc-context.js";
7
+ export {
8
+ defineComponent,
9
+ textOf,
10
+ type ComponentMeta,
11
+ type MdxrComponent,
12
+ } from "./define.js";
13
+
14
+ // shadcn/ui (Base UI) component set — composable from project components.
15
+ export * from "./components/ui/accordion.js";
16
+ export * from "./components/ui/alert.js";
17
+ export * from "./components/ui/alert-dialog.js";
18
+ export * from "./components/ui/aspect-ratio.js";
19
+ export * from "./components/ui/attachment.js";
20
+ export * from "./components/ui/avatar.js";
21
+ export * from "./components/ui/badge.js";
22
+ export * from "./components/ui/breadcrumb.js";
23
+ export * from "./components/ui/bubble.js";
24
+ export * from "./components/ui/button.js";
25
+ export * from "./components/ui/button-group.js";
26
+ export * from "./components/ui/calendar.js";
27
+ export * from "./components/ui/card.js";
28
+ export * from "./components/ui/carousel.js";
29
+ export * from "./components/ui/chart.js";
30
+ export * from "./components/ui/checkbox.js";
31
+ export * from "./components/ui/collapsible.js";
32
+ export * from "./components/ui/combobox.js";
33
+ export * from "./components/ui/command.js";
34
+ export * from "./components/ui/context-menu.js";
35
+ export * from "./components/ui/dialog.js";
36
+ export * from "./components/ui/direction.js";
37
+ export * from "./components/ui/drawer.js";
38
+ export * from "./components/ui/dropdown-menu.js";
39
+ export * from "./components/ui/empty.js";
40
+ export * from "./components/ui/field.js";
41
+ export * from "./components/ui/fieldset.js";
42
+ export * from "./components/ui/frame.js";
43
+ export * from "./components/ui/hover-card.js";
44
+ export * from "./components/ui/input.js";
45
+ export * from "./components/ui/input-group.js";
46
+ export * from "./components/ui/input-otp.js";
47
+ export * from "./components/ui/item.js";
48
+ export * from "./components/ui/kbd.js";
49
+ export * from "./components/ui/label.js";
50
+ export * from "./components/ui/marker.js";
51
+ export * from "./components/ui/menubar.js";
52
+ export * from "./components/ui/message.js";
53
+ export * from "./components/ui/message-scroller.js";
54
+ export * from "./components/ui/meter.js";
55
+ export * from "./components/ui/native-select.js";
56
+ export * from "./components/ui/navigation-menu.js";
57
+ export * from "./components/ui/pagination.js";
58
+ export * from "./components/ui/popover.js";
59
+ export * from "./components/ui/progress.js";
60
+ export * from "./components/ui/questionnaire.js";
61
+ export * from "./components/ui/radio-group.js";
62
+ export * from "./components/ui/resizable.js";
63
+ export * from "./components/ui/scroll-area.js";
64
+ export * from "./components/ui/select.js";
65
+ export * from "./components/ui/separator.js";
66
+ export * from "./components/ui/sheet.js";
67
+ export * from "./components/ui/sidebar.js";
68
+ export * from "./components/ui/skeleton.js";
69
+ export * from "./components/ui/slider.js";
70
+ export * from "./components/ui/spinner.js";
71
+ export * from "./components/ui/switch.js";
72
+ export * from "./components/ui/table.js";
73
+ export * from "./components/ui/tabs.js";
74
+ export * from "./components/ui/textarea.js";
75
+ export * from "./components/ui/toast.js";
76
+ export * from "./components/ui/toggle.js";
77
+ export * from "./components/ui/toggle-group.js";
78
+ export * from "./components/ui/tooltip.js";
package/src/config.ts ADDED
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { isRecord, nonEmpty } from "./guards.js";
5
+ import { loadUserModule } from "./load-user-module.js";
6
+
7
+ /** Shape of `mdxr.config.ts` in a project that uses mdxr. */
8
+ export interface MdxrConfig {
9
+ /** Path to a TS/TSX module whose named exports are extra MDX components. */
10
+ components?: string;
11
+ /**
12
+ * Editor used for file links (`vscode` default). A known name (`cursor`,
13
+ * `zed`, `vscode-insiders`, `windsurf`, `sublime`, `textmate`, `idea`), a
14
+ * `{path}`/`{line}` URL template, or `"none"` to disable. Frontmatter
15
+ * `editor:` overrides per document.
16
+ */
17
+ editor?: string;
18
+ /** Path to a CSS file with `@theme` overrides / extra utilities. */
19
+ theme?: string;
20
+ }
21
+
22
+ export const defineConfig = (config: MdxrConfig): MdxrConfig => config;
23
+
24
+ export interface ResolvedConfig {
25
+ componentsPath?: string;
26
+ themePath?: string;
27
+ /** Bundled mdxr.config code (Tailwind scan source). */
28
+ componentsCode?: string;
29
+ dir: string;
30
+ editor?: string;
31
+ }
32
+
33
+ const CONFIG_FILES = [
34
+ "mdxr.config.ts",
35
+ "mdxr.config.mts",
36
+ "mdxr.config.js",
37
+ "mdxr.config.mjs",
38
+ ];
39
+
40
+ export const loadConfig = async (dir: string): Promise<ResolvedConfig> => {
41
+ const name = CONFIG_FILES.find((n) => fs.existsSync(path.join(dir, n)));
42
+ if (name === undefined) {
43
+ return { dir };
44
+ }
45
+ const { module: mod, code } = await loadUserModule(path.join(dir, name));
46
+ const raw = isRecord(mod.default) ? mod.default : {};
47
+ return {
48
+ componentsCode: code,
49
+ componentsPath: nonEmpty(raw.components)
50
+ ? path.resolve(dir, raw.components)
51
+ : undefined,
52
+ dir,
53
+ editor: nonEmpty(raw.editor) ? raw.editor : undefined,
54
+ themePath: nonEmpty(raw.theme) ? path.resolve(dir, raw.theme) : undefined,
55
+ };
56
+ };
package/src/define.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { Fragment, isValidElement } from "react";
2
+ import type { ReactElement, ReactNode } from "react";
3
+ import type { GenericSchema, InferOutput } from "valibot";
4
+ import { safeParse } from "valibot";
5
+
6
+ import { isRecord } from "./guards.js";
7
+
8
+ /** Metadata attached to a component for `mdxr catalog` and prop validation. */
9
+ export interface ComponentMeta {
10
+ description?: string;
11
+ /** Valibot schema for props (MDX attributes arrive as strings; `children` is passed through). */
12
+ schema?: GenericSchema;
13
+ }
14
+
15
+ /** Props as received from a document: arbitrary attributes plus children. */
16
+ export interface DocProps extends Record<string, unknown> {
17
+ children?: ReactNode;
18
+ }
19
+
20
+ /**
21
+ * A component usable in an mdxr document. Props are intentionally loose —
22
+ * documents are data, and prop validation happens via `defineComponent`
23
+ * schemas at render time. Only ever invoked by MDX/React.
24
+ */
25
+ export type AnyComponent = ((props: DocProps) => ReactNode) & {
26
+ __mdxr?: ComponentMeta;
27
+ };
28
+
29
+ /** Name → component map passed to MDX's `components` prop. */
30
+ export type ComponentMap = Record<string, AnyComponent>;
31
+
32
+ export type MdxrComponent = AnyComponent;
33
+
34
+ type PropsOf<S> = S extends GenericSchema
35
+ ? InferOutput<S>
36
+ : Record<string, unknown>;
37
+
38
+ /**
39
+ * Validate `props` against `schema`, throwing the catalog's `Invalid props`
40
+ * error on failure. `tag` names the element in the message
41
+ * (`Invalid props on <Node>`); without it the bare `Invalid props` form is
42
+ * reported — the shape `defineComponent` produces.
43
+ */
44
+ export const parseProps = <S extends GenericSchema>(
45
+ schema: S,
46
+ props: unknown,
47
+ tag?: string
48
+ ): InferOutput<S> => {
49
+ const r = safeParse(schema, props);
50
+ if (!r.success) {
51
+ const detail = r.issues
52
+ .map(
53
+ (i) =>
54
+ `${i.path?.map((p) => String(p.key)).join(".") ?? "props"}: ${i.message}`
55
+ )
56
+ .join("; ");
57
+ throw new Error(
58
+ tag === undefined
59
+ ? `Invalid props: ${detail}`
60
+ : `Invalid props on <${tag}>: ${detail}`
61
+ );
62
+ }
63
+ return r.output;
64
+ };
65
+
66
+ /**
67
+ * Wrap a render function with prop validation + catalog metadata.
68
+ * Prop types are inferred from the valibot schema, so the render callback
69
+ * is fully typed. Validation failures surface as document errors so the
70
+ * agent can self-correct.
71
+ */
72
+ export const defineComponent = <
73
+ S extends GenericSchema | undefined = undefined,
74
+ >(
75
+ meta: { description?: string; schema?: S },
76
+ render: (props: PropsOf<S> & { children?: ReactNode }) => ReactElement | null
77
+ ): MdxrComponent => {
78
+ const Comp = (props: DocProps): ReactElement | null => {
79
+ const parsed: unknown =
80
+ meta.schema === undefined ? props : parseProps(meta.schema, props);
81
+ // Validated above by the schema (or intentionally loose without one).
82
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
83
+ return render(parsed as PropsOf<S> & { children?: ReactNode });
84
+ };
85
+ Comp.__mdxr = meta;
86
+ return Comp;
87
+ };
88
+
89
+ /**
90
+ * Normalize a `children` prop into a flat array: nested arrays and fragments
91
+ * are unwrapped, and null/undefined/boolean nodes are dropped. Used instead of
92
+ * `React.Children` utilities, which lint rules discourage.
93
+ */
94
+ export const flattenChildren = (node: ReactNode): ReactNode[] => {
95
+ if (Array.isArray(node)) {
96
+ return node.flatMap(flattenChildren);
97
+ }
98
+ if (node === null || node === undefined || typeof node === "boolean") {
99
+ return [];
100
+ }
101
+ if (
102
+ isValidElement<{ children?: ReactNode }>(node) &&
103
+ node.type === Fragment
104
+ ) {
105
+ return flattenChildren(node.props.children);
106
+ }
107
+ return [node];
108
+ };
109
+
110
+ /** Extract all text from a React node tree (used for copy-to-clipboard payloads). */
111
+ export const textOf = (node: unknown): string => {
112
+ if (typeof node === "string" || typeof node === "number") {
113
+ return String(node);
114
+ }
115
+ if (Array.isArray(node)) {
116
+ return node.map(textOf).join("");
117
+ }
118
+ if (isRecord(node) && isRecord(node.props)) {
119
+ return textOf(node.props.children);
120
+ }
121
+ return "";
122
+ };
@@ -0,0 +1,20 @@
1
+ import { createContext } from "react";
2
+
3
+ export interface DocContextValue {
4
+ /**
5
+ * Build an editor URL (`vscode://file/…`, per the resolved `editor` setting)
6
+ * for a document-relative path, or undefined when file links are disabled or
7
+ * the file does not exist on disk.
8
+ */
9
+ fileLink?: (relPath: string, line?: string) => string | undefined;
10
+ /**
11
+ * The document's render timestamp. Relative-time components (`<Due>`) must
12
+ * read "now" from here: the value is serialized into the hydration payload,
13
+ * so client rendering sees the same instant SSR did — a render crossing
14
+ * midnight can't produce a hydration mismatch.
15
+ */
16
+ now?: Date;
17
+ }
18
+
19
+ /** Render-time document info shared with components. */
20
+ export const DocContext = createContext<DocContextValue>({});
package/src/editor.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { nonEmpty, urlScheme } from "./guards.js";
2
+
3
+ type EditorLink = (absPath: string, line?: string) => string;
4
+
5
+ /**
6
+ * Path → URI path segment-by-segment. `encodeURI` would leave `#`, `?`,
7
+ * `%`, `&` intact and they parse as delimiters inside the editor URL;
8
+ * `encodeURIComponent` on the whole path would mangle `/`. Backslashes are
9
+ * normalized so Windows paths survive too (`C:\x` → `C%3A/x`).
10
+ */
11
+ const encodePath = (p: string): string =>
12
+ p
13
+ .replaceAll("\\", "/")
14
+ .split("/")
15
+ .map((seg) => encodeURIComponent(seg))
16
+ .join("/");
17
+
18
+ /** `scheme://file/{abs}:{line}` — the VS Code URL convention most editors share. */
19
+ const atLine =
20
+ (scheme: string): EditorLink =>
21
+ (p, l) =>
22
+ `${scheme}://file${encodePath(p)}${nonEmpty(l) ? `:${l}` : ""}`;
23
+
24
+ /** `{scheme}://open?url=file://{abs}&line={n}` — Sublime/TextMate style. */
25
+ const queryUrl =
26
+ (scheme: string): EditorLink =>
27
+ (p, l) =>
28
+ `${scheme}://open?url=file://${encodePath(p)}${nonEmpty(l) ? `&line=${l}` : ""}`;
29
+
30
+ /** Known editors; unknown names fall back to the `scheme://file` convention. */
31
+ export const EDITORS: Record<string, EditorLink> = {
32
+ cursor: atLine("cursor"),
33
+ idea: (p, l) =>
34
+ `idea://open?file=${encodePath(p)}${nonEmpty(l) ? `&line=${l}` : ""}`,
35
+ sublime: queryUrl("subl"),
36
+ textmate: queryUrl("txmt"),
37
+ vscode: atLine("vscode"),
38
+ "vscode-insiders": atLine("vscode-insiders"),
39
+ windsurf: atLine("windsurf"),
40
+ zed: atLine("zed"),
41
+ };
42
+
43
+ /**
44
+ * Schemes that execute script when an `<a href>` is clicked. `editor` is a
45
+ * config value, but frontmatter can also supply it — a document must not be
46
+ * able to mint `javascript:`/`data:` links through file references.
47
+ */
48
+ const SCRIPTABLE = new Set(["data", "javascript", "vbscript"]);
49
+
50
+ const build = (
51
+ editor: string | undefined,
52
+ absPath: string,
53
+ line?: string
54
+ ): string | undefined => {
55
+ const e = nonEmpty(editor) ? editor : "vscode";
56
+ if (e === "none") {
57
+ return undefined;
58
+ }
59
+ const known = EDITORS[e];
60
+ if (known !== undefined) {
61
+ return known(absPath, line);
62
+ }
63
+ if (e.includes("{path}")) {
64
+ return e
65
+ .replaceAll("{path}", encodePath(absPath))
66
+ .replaceAll("{line}", line ?? "");
67
+ }
68
+ // A bare name becomes a `scheme://file` editor — but only if it could
69
+ // actually be a URI scheme; otherwise the link would be malformed.
70
+ if (!/^[a-z][a-z0-9+.-]*$/iu.test(e)) {
71
+ return undefined;
72
+ }
73
+ return atLine(e)(absPath, line);
74
+ };
75
+
76
+ /**
77
+ * Editor URL for an absolute path. `editor` is a known name (see EDITORS),
78
+ * `"none"` (no link), a URL template with `{path}`/`{line}` placeholders
79
+ * (`myed://open?f={path}&l={line}`), or a bare name treated as a
80
+ * `scheme://file` editor. Undefined → vscode. Scriptable schemes are
81
+ * refused — `editor` can arrive from frontmatter, which is document input.
82
+ */
83
+ export const editorUrl = (
84
+ editor: string | undefined,
85
+ absPath: string,
86
+ line?: string
87
+ ): string | undefined => {
88
+ const url = build(editor, absPath, line);
89
+ if (url === undefined) {
90
+ return undefined;
91
+ }
92
+ const scheme = urlScheme(url);
93
+ return scheme !== undefined && SCRIPTABLE.has(scheme) ? undefined : url;
94
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Validate the `--format` flag: omitted and "text" mean human-readable
3
+ * errors, "json" switches failure output to a machine-readable object.
4
+ */
5
+ export const parseErrorFormat = (format?: string): "text" | "json" => {
6
+ if (format === undefined || format === "text") {
7
+ return "text";
8
+ }
9
+ if (format === "json") {
10
+ return "json";
11
+ }
12
+ throw new Error(`invalid --format "${format}" (expected text|json)`);
13
+ };
14
+
15
+ export const formatError = (err: unknown): string => {
16
+ if (err instanceof Error) {
17
+ // vfile-style messages carry line/column for agent self-repair.
18
+ const e = err as Error & { line?: number; column?: number; file?: string };
19
+ const loc =
20
+ e.line === null || e.line === undefined
21
+ ? ""
22
+ : `:${e.line}:${e.column ?? 0}`;
23
+ const file = e.file ?? "";
24
+ return `${file}${loc} ${e.message}`.trim();
25
+ }
26
+ return String(err);
27
+ };
28
+
29
+ const levenshtein = (a: string, b: string): number => {
30
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [
31
+ i,
32
+ ...Array.from({ length: b.length }, () => 0),
33
+ ]);
34
+ for (let j = 1; j <= b.length; j += 1) {
35
+ dp[0][j] = j;
36
+ }
37
+ for (let i = 1; i <= a.length; i += 1) {
38
+ for (let j = 1; j <= b.length; j += 1) {
39
+ dp[i][j] = Math.min(
40
+ dp[i - 1][j] + 1,
41
+ dp[i][j - 1] + 1,
42
+ dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
43
+ );
44
+ }
45
+ }
46
+ return dp[a.length][b.length];
47
+ };
48
+
49
+ /**
50
+ * MDX's "Expected component `<X>`" errors get a friendlier message: the
51
+ * closest catalog name (edit distance ≤ 3) plus the full list, so the agent
52
+ * can self-correct without a `mdxr catalog` round-trip.
53
+ */
54
+ export const enhanceRenderError = (
55
+ err: unknown,
56
+ componentNames: string[]
57
+ ): unknown => {
58
+ if (!(err instanceof Error)) {
59
+ return err;
60
+ }
61
+ const m = /Expected component [`"'](?<name>\w+)[`"']/u.exec(err.message);
62
+ const name = m?.groups?.name;
63
+ if (name === undefined) {
64
+ return err;
65
+ }
66
+ const names = componentNames.filter((n) => /^[A-Z]/u.test(n));
67
+ const [nearest] = names
68
+ .map((n) => ({ d: levenshtein(name.toLowerCase(), n.toLowerCase()), n }))
69
+ .toSorted((x, y) => x.d - y.d);
70
+ const hint =
71
+ nearest !== undefined && nearest.d <= 3
72
+ ? ` Did you mean <${nearest.n}>?`
73
+ : "";
74
+ return new Error(
75
+ `Unknown component <${name}>.${hint} Available: ${names.join(", ")}. Add custom components via mdxr.config.ts.`,
76
+ { cause: err }
77
+ );
78
+ };
package/src/guards.ts ADDED
@@ -0,0 +1,84 @@
1
+ /** Runtime type guards shared across AST/config/module boundaries. */
2
+
3
+ import type { AnyComponent } from "./define.js";
4
+
5
+ export const isRecord = (v: unknown): v is Record<string, unknown> =>
6
+ typeof v === "object" && v !== null;
7
+
8
+ /** `React.memo`/`forwardRef`/`lazy` results are objects, not functions —
9
+ * but they are still valid element types for the MDX components map. */
10
+ const REACT_COMPONENT_OBJECTS = new Set<symbol>([
11
+ Symbol.for("react.memo"),
12
+ Symbol.for("react.forward_ref"),
13
+ Symbol.for("react.lazy"),
14
+ ]);
15
+
16
+ export const isComponent = (v: unknown): v is AnyComponent =>
17
+ typeof v === "function" ||
18
+ (isRecord(v) &&
19
+ typeof v.$$typeof === "symbol" &&
20
+ REACT_COMPONENT_OBJECTS.has(v.$$typeof));
21
+
22
+ /** Treat "" as absent — document attributes arrive as strings. */
23
+ export const nonEmpty = (v: unknown): v is string =>
24
+ typeof v === "string" && v !== "";
25
+
26
+ /** String-or-undefined narrow — unlike `nonEmpty`, "" passes through. */
27
+ export const asString = (v: unknown): string | undefined =>
28
+ typeof v === "string" ? v : undefined;
29
+
30
+ /** Membership-guard factory: `isOneOf(STATUSES)` narrows to the list's union. */
31
+ export const isOneOf =
32
+ <T extends string>(list: readonly T[]) =>
33
+ (x: unknown): x is T =>
34
+ typeof x === "string" && (list as readonly string[]).includes(x);
35
+
36
+ /**
37
+ * Prototype-safe record lookup — `table[key]` only for own properties, so
38
+ * keys like "toString" or "constructor" can't leak members off `Object`'s
39
+ * prototype. Use wherever document-supplied strings index a table.
40
+ */
41
+ export const own = <T>(
42
+ table: Readonly<Record<string, T>>,
43
+ key: string
44
+ ): T | undefined => (Object.hasOwn(table, key) ? table[key] : undefined);
45
+
46
+ const SAFE_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
47
+
48
+ /**
49
+ * The scheme a browser would parse out of `v`, lowercased — or undefined for
50
+ * scheme-less values. Browsers strip \t\n\r anywhere in a URL and remove
51
+ * leading/trailing C0 controls and spaces before scheme parsing
52
+ * (" javascript:…", "java\tscript:…", "\x01javascript:…" all execute), so
53
+ * the probe uses the same normalized form.
54
+ */
55
+ export const urlScheme = (v: string): string | undefined => {
56
+ /* oxlint-disable no-control-regex -- C0 range is the point: the URL parser
57
+ strips it at the edges, so the probe must too */
58
+ const probe = v
59
+ .replaceAll(/[\t\n\r]/gu, "")
60
+ .replace(/^[\u0000-\u0020]+/u, "")
61
+ .replace(/[\u0000-\u0020]+$/u, "");
62
+ /* oxlint-enable no-control-regex */
63
+ return /^(?<scheme>[a-zA-Z][a-zA-Z0-9+.-]*):/u
64
+ .exec(probe)
65
+ ?.groups?.scheme.toLowerCase();
66
+ };
67
+
68
+ /**
69
+ * Doc-supplied `href` props pass through to `<a href>` unescaped — a
70
+ * `javascript:`/`data:` scheme would execute on click. Allow only web
71
+ * schemes plus scheme-less values (anchors, relative and protocol-relative
72
+ * URLs); anything else renders as no link. Editor URLs (`vscode:` etc.)
73
+ * never take this path — they come from the server-side `fileLink`.
74
+ */
75
+ export const safeHref = (v: unknown): string | undefined => {
76
+ if (typeof v !== "string" || v === "") {
77
+ return undefined;
78
+ }
79
+ const scheme = urlScheme(v);
80
+ if (scheme === undefined || SAFE_SCHEMES.has(scheme)) {
81
+ return v;
82
+ }
83
+ return undefined;
84
+ };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+
3
+ const MOBILE_BREAKPOINT = 768;
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>();
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
12
+ };
13
+ mql.addEventListener("change", onChange);
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
15
+ return () => {
16
+ mql.removeEventListener("change", onChange);
17
+ };
18
+ }, []);
19
+
20
+ return !!isMobile;
21
+ }