@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,537 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+
3
+ import { nonEmpty } from "../guards.js";
4
+ import { indexChildren } from "./child-index.js";
5
+ import { linkTarget, useFileLink } from "./file-link.js";
6
+ import { Icon } from "./icon.js";
7
+ import {
8
+ BORDER_CLS,
9
+ CHIP_BORDER_CLS,
10
+ commentStripCls,
11
+ LOC_CLS,
12
+ SURFACE_CLS,
13
+ TEXT,
14
+ TEXT_MICRO,
15
+ TEXT_SUB,
16
+ TONE_TEXT,
17
+ TRIM_CLS,
18
+ } from "./tones.js";
19
+
20
+ /**
21
+ * Bordered panel shape framing a document block (code, diffs, graphs,
22
+ * tables). Exported for panels needing a non-figure tag (`<nav>`, `<section>`)
23
+ * or extra attributes; plain panels should use `<Panel>`.
24
+ */
25
+ export const PANEL_CLS = `not-prose my-6 overflow-hidden rounded-lg border ${BORDER_CLS}`;
26
+
27
+ export const Panel = (props: {
28
+ children?: ReactNode;
29
+ /** Extra classes (e.g. `mdxr-json`) merged after the panel shape. */
30
+ className?: string;
31
+ }): ReactElement => (
32
+ <figure className={`${PANEL_CLS} ${props.className ?? ""}`}>
33
+ {props.children}
34
+ </figure>
35
+ );
36
+
37
+ // Lookup maps (not `items-${x}` interpolation) so Tailwind's static class
38
+ // scan sees the literals.
39
+ const ROW_ALIGN = {
40
+ baseline: "items-baseline",
41
+ center: "items-center",
42
+ } as const;
43
+ const ROW_GAP_X = {
44
+ "2": "gap-x-2",
45
+ "2.5": "gap-x-2.5",
46
+ "3": "gap-x-3",
47
+ } as const;
48
+
49
+ /** One padded row inside a `ListPanel`. */
50
+ export const ListRow = (props: {
51
+ /** `items-*` — default `items-baseline`. */
52
+ align?: keyof typeof ROW_ALIGN;
53
+ children?: ReactNode;
54
+ /** Non-conflicting extras, e.g. `opacity-60`. */
55
+ className?: string;
56
+ /** `gap-x-*` — default `gap-x-3`. */
57
+ gapX?: keyof typeof ROW_GAP_X;
58
+ }): ReactElement => (
59
+ <div
60
+ className={`flex flex-wrap ${ROW_ALIGN[props.align ?? "baseline"]} ${ROW_GAP_X[props.gapX ?? "3"]} gap-y-1 px-4 py-2.5 ${props.className ?? ""}`}
61
+ >
62
+ {props.children}
63
+ </div>
64
+ );
65
+
66
+ /**
67
+ * Click-to-copy button. The `mdxr-copy`/`mdxr-copy-idle`/`mdxr-copy-done`
68
+ * classes are client-JS hooks (assets.ts toggles `.copied` on the button) —
69
+ * rename them and copy stops working.
70
+ */
71
+ export const CopyButton = (props: {
72
+ /** Extra button classes (default `opacity-60`). */
73
+ className?: string;
74
+ /** Clipboard payload (`data-copy`). */
75
+ copy: string;
76
+ /** Done-state color classes (default light+dark emerald). */
77
+ doneClassName?: string;
78
+ /** Tooltip + accessible label. */
79
+ title: string;
80
+ }): ReactElement => (
81
+ <button
82
+ aria-label={props.title}
83
+ className={`mdxr-copy cursor-pointer ${props.className ?? "opacity-60"}`}
84
+ data-copy={props.copy}
85
+ title={props.title}
86
+ type="button"
87
+ >
88
+ <span className="mdxr-copy-idle inline-flex">
89
+ <Icon className="h-3.5 w-3.5" name="lucide:copy" />
90
+ </span>
91
+ <span
92
+ className={`mdxr-copy-done hidden items-center ${props.doneClassName ?? TONE_TEXT.emerald}`}
93
+ >
94
+ <Icon className="h-3.5 w-3.5" name="lucide:check" />
95
+ </span>
96
+ </button>
97
+ );
98
+
99
+ /**
100
+ * Bordered secondary-button shape for labeled copy/save actions (Ask,
101
+ * Board). `transition-colors` only — `transition-all` would animate layout
102
+ * properties too.
103
+ */
104
+ export const ACTION_BUTTON_CLS = `inline-flex cursor-pointer items-center rounded-md border ${CHIP_BORDER_CLS} bg-white px-2.5 py-1 text-xs font-medium ${TEXT.chip} transition-colors hover:bg-neutral-100 active:translate-y-px dark:bg-neutral-800 dark:hover:bg-neutral-700`;
105
+
106
+ /**
107
+ * Idle/done label pair inside a labeled action button. The
108
+ * `mdxr-copy-idle`/`mdxr-copy-done` classes are client-JS hooks — the
109
+ * document script toggles `.copied` on the parent button to swap them.
110
+ */
111
+ export const CopyFeedback = (props: {
112
+ /** Done-state text (e.g. `Copied`, `Saved`). */
113
+ done: string;
114
+ /** Idle-state icon, e.g. `lucide:clipboard-list`. */
115
+ icon: string;
116
+ /** Idle-state text (e.g. `Copy answers`). */
117
+ label: string;
118
+ }): ReactElement => (
119
+ <>
120
+ <span className="mdxr-copy-idle inline-flex items-center gap-1.5">
121
+ <Icon className="h-3.5 w-3.5" name={props.icon} />
122
+ {props.label}
123
+ </span>
124
+ <span
125
+ className={`mdxr-copy-done hidden items-center gap-1.5 ${TONE_TEXT.emerald}`}
126
+ >
127
+ <Icon className="h-3.5 w-3.5" name="lucide:check" />
128
+ {props.done}
129
+ </span>
130
+ </>
131
+ );
132
+
133
+ /** Round status/kind chip — icon + label on a `TONE` background. */
134
+ export const Pill = (props: {
135
+ children?: ReactNode;
136
+ /** Tone/color classes appended after the pill shape. */
137
+ className?: string;
138
+ /** Iconify name, e.g. `lucide:check`. */
139
+ icon?: string;
140
+ }): ReactElement => (
141
+ <span
142
+ className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${props.className ?? ""}`}
143
+ >
144
+ {props.icon === undefined ? null : (
145
+ <Icon className="h-3 w-3" name={props.icon} />
146
+ )}
147
+ {props.children}
148
+ </span>
149
+ );
150
+
151
+ const CAPTION_BASE = `${SURFACE_CLS} px-4 py-2 text-xs ${TEXT.muted}`;
152
+ const CAPTION_BORDER = `border-b ${BORDER_CLS}`;
153
+
154
+ /** CaptionBar's strip classes, for headers that are not a `<figcaption>`
155
+ * (Ask's section header). */
156
+ export const CAPTION_CLS = `${CAPTION_BORDER} ${CAPTION_BASE}`;
157
+
158
+ /**
159
+ * Header strip on framed blocks (code, diffs, tables). `border` (default on)
160
+ * draws the divider to the body; `className` adds layout like
161
+ * `flex items-center gap-2` or weight like `font-medium`/`font-mono`.
162
+ */
163
+ export const CaptionBar = (props: {
164
+ border?: boolean;
165
+ children?: ReactNode;
166
+ className?: string;
167
+ }): ReactElement => (
168
+ <figcaption
169
+ className={`${props.border === false ? "" : `${CAPTION_BORDER} `}${CAPTION_BASE} ${props.className ?? ""}`}
170
+ >
171
+ {props.children}
172
+ </figcaption>
173
+ );
174
+
175
+ /** PANEL_CLS + row dividers — the bordered container for row-style lists. */
176
+ const LIST_CLS = `divide-y divide-neutral-200 dark:divide-neutral-800 ${PANEL_CLS}`;
177
+
178
+ /**
179
+ * Bordered `divide-y` container for row-style lists (Deps, Files, Tests…).
180
+ * `as` picks the semantic tag; figures are the default because captioned
181
+ * lists pair this with `CaptionBar` (a figcaption). `title` renders that
182
+ * caption — only sensible on the default figure.
183
+ */
184
+ export const ListPanel = (props: {
185
+ as?: "div" | "dl" | "figure" | "section";
186
+ children?: ReactNode;
187
+ className?: string;
188
+ /** Plain-text list title rendered as a `CaptionBar`. */
189
+ title?: string;
190
+ }): ReactElement => {
191
+ const Tag = props.as ?? "figure";
192
+ return (
193
+ <Tag className={`${LIST_CLS} ${props.className ?? ""}`}>
194
+ {nonEmpty(props.title) ? (
195
+ <CaptionBar border={false} className="font-medium">
196
+ {props.title}
197
+ </CaptionBar>
198
+ ) : null}
199
+ {props.children}
200
+ </Tag>
201
+ );
202
+ };
203
+
204
+ /**
205
+ * Truncated name + optional muted sub-line — the label cell of chart rows
206
+ * (Waterfall spans, Gantt tasks/milestones).
207
+ */
208
+ export const RowLabel = (props: {
209
+ name: ReactNode;
210
+ /** Faint second line (`owner · note`); empty/absent hides it. */
211
+ sub?: string;
212
+ }): ReactElement => (
213
+ <div className="min-w-0">
214
+ <div className="truncate text-sm">{props.name}</div>
215
+ {nonEmpty(props.sub) ? (
216
+ <div className={`truncate ${TEXT_SUB} ${TEXT.faint}`}>{props.sub}</div>
217
+ ) : null}
218
+ </div>
219
+ );
220
+
221
+ /**
222
+ * Rotating disclosure chevron for tree-line `<details>` (Tree, Json).
223
+ * `mdxr-chev` is the CSS hook that spins it open — keep the name.
224
+ */
225
+ export const FoldChev = (props: {
226
+ /** Non-conflicting extras, e.g. group-hover colors. */
227
+ className?: string;
228
+ }): ReactElement => (
229
+ <Icon
230
+ className={`mdxr-chev h-3 w-3 shrink-0 self-center ${TEXT.faint} ${props.className ?? ""}`}
231
+ name="lucide:chevron-right"
232
+ />
233
+ );
234
+
235
+ /** Muted icon centered on a list row (Files/Deps/Changes/Search). */
236
+ export const RowIcon = (props: {
237
+ /** Non-conflicting extras. */
238
+ className?: string;
239
+ /** Iconify name, e.g. `lucide:search`. */
240
+ name: string;
241
+ }): ReactElement => (
242
+ <Icon
243
+ className={`h-3.5 w-3.5 shrink-0 self-center ${TEXT.faint} ${props.className ?? ""}`}
244
+ name={props.name}
245
+ />
246
+ );
247
+
248
+ /**
249
+ * Inline kind/status tag — icon + label colored via `className`
250
+ * (e.g. `text-emerald-600 dark:text-emerald-400`); no background, unlike `Pill`.
251
+ */
252
+ export const Tag = (props: {
253
+ children?: ReactNode;
254
+ /** Tone/extra classes appended after the tag shape. */
255
+ className?: string;
256
+ /** Iconify name. */
257
+ icon: string;
258
+ }): ReactElement => (
259
+ <span
260
+ className={`inline-flex shrink-0 items-center gap-1 text-xs font-medium ${props.className ?? ""}`}
261
+ >
262
+ <Icon className="h-3.5 w-3.5" name={props.icon} />
263
+ {props.children}
264
+ </span>
265
+ );
266
+
267
+ /** Top-level titled section — the `my-6` wrapper with an optional h3 heading. */
268
+ export const Section = (props: {
269
+ children?: ReactNode;
270
+ /** Heading text; empty/absent hides the h3. */
271
+ title?: string;
272
+ }): ReactElement => (
273
+ <section className="my-6">
274
+ {nonEmpty(props.title) ? <h3 className="mt-0">{props.title}</h3> : null}
275
+ {props.children}
276
+ </section>
277
+ );
278
+
279
+ /** Small numbered square used by list items indexed via `useChildIndex`. */
280
+ export const NumBadge = (props: { n: number }): ReactElement => (
281
+ <span
282
+ className={`flex h-5 w-5 items-center justify-center rounded-md border ${CHIP_BORDER_CLS} font-mono ${TEXT_MICRO} font-medium ${TEXT.faint}`}
283
+ >
284
+ {props.n}
285
+ </span>
286
+ );
287
+
288
+ /**
289
+ * Optional MDX body region — renders nothing without children and trims the
290
+ * outer margins of the first/last child (TRIM_CLS). `className` carries the
291
+ * spacing/text look, e.g. `mt-1.5 text-sm`.
292
+ */
293
+ export const TrimBody = (props: {
294
+ children?: ReactNode;
295
+ className?: string;
296
+ }): ReactElement | null =>
297
+ props.children === undefined ? null : (
298
+ <div className={`${TRIM_CLS} ${props.className ?? ""}`}>
299
+ {props.children}
300
+ </div>
301
+ );
302
+
303
+ /**
304
+ * Bordered card for counted list items (Finding, Hypothesis): index badge +
305
+ * a pill slot + optional title + prose children.
306
+ */
307
+ export const IndexedCard = (props: {
308
+ /** Right-aligned extra in the header row (e.g. a file LocLink). */
309
+ aside?: ReactNode;
310
+ children?: ReactNode;
311
+ /** 1-based index from `useChildIndex`; 0 hides the badge. */
312
+ n: number;
313
+ /** Status pill (usually `<Pill>`). */
314
+ pill: ReactNode;
315
+ title?: string;
316
+ }): ReactElement => (
317
+ <article className={`rounded-lg border px-4 py-3 ${BORDER_CLS}`}>
318
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
319
+ {props.n > 0 ? <NumBadge n={props.n} /> : null}
320
+ {props.pill}
321
+ {nonEmpty(props.title) ? (
322
+ <span className="text-sm font-medium">{props.title}</span>
323
+ ) : null}
324
+ {props.aside === undefined ? null : (
325
+ <span className="ml-auto">{props.aside}</span>
326
+ )}
327
+ </div>
328
+ <TrimBody className="mt-1.5 text-sm">{props.children}</TrimBody>
329
+ </article>
330
+ );
331
+
332
+ /**
333
+ * Section wrapper for counted item lists (Findings, Hypotheses): optional
334
+ * heading, a `N items · n status` summary line, then indexed children.
335
+ */
336
+ export const CountedList = (props: {
337
+ children?: ReactNode;
338
+ /** prop value → count, e.g. `{confirmed: 2, inferred: 1}`. */
339
+ counts: ReadonlyMap<string, number>;
340
+ /** Display label for a count key (rendered lowercased by callers). */
341
+ labelOf: (key: string) => string;
342
+ /** `[singular, plural]` noun for the total line. */
343
+ noun: readonly [string, string];
344
+ /** Fixed display order for the per-key counts. */
345
+ order: readonly string[];
346
+ title?: string;
347
+ }): ReactElement => {
348
+ const total = [...props.counts.values()].reduce((a, b) => a + b, 0);
349
+ return (
350
+ <Section title={props.title}>
351
+ {total > 0 ? (
352
+ <div
353
+ className={`mb-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs ${TEXT.muted}`}
354
+ >
355
+ <span>
356
+ {total} {props.noun[total === 1 ? 0 : 1]}
357
+ </span>
358
+ {props.order.map((k) => {
359
+ const count = props.counts.get(k);
360
+ return count === undefined ? null : (
361
+ <span key={k}>
362
+ · {count} {props.labelOf(k)}
363
+ </span>
364
+ );
365
+ })}
366
+ </div>
367
+ ) : null}
368
+ <div className="not-prose space-y-3">{indexChildren(props.children)}</div>
369
+ </Section>
370
+ );
371
+ };
372
+
373
+ /**
374
+ * Hover "+" on a code/diff row (`<Comments>`) — opens the comment form
375
+ * under that line. `data-comment-add` carries the line number, `data-side`/
376
+ * `data-file` the diff anchor; `.mdxr-add` positions and reveals the
377
+ * button (BASE_CSS). All are client-JS hooks — rename them and adding
378
+ * comments stops working.
379
+ */
380
+ export const AddCommentButton = (props: {
381
+ file?: string;
382
+ line: number;
383
+ side?: "new" | "old";
384
+ }): ReactElement => (
385
+ <button
386
+ aria-label={`Add a comment on line ${props.line}`}
387
+ className="mdxr-add"
388
+ data-comment-add={props.line}
389
+ data-file={props.file}
390
+ data-side={props.side === "old" ? "old" : undefined}
391
+ title="Add a comment"
392
+ type="button"
393
+ >
394
+ <Icon className="h-3 w-3" name="lucide:plus" />
395
+ </button>
396
+ );
397
+
398
+ /**
399
+ * Reply affordance at the end of a comment thread. `data-comment-reply`
400
+ * is a client-JS hook (doc-events.ts swaps it for the comment form) —
401
+ * rename it and replies stop opening.
402
+ */
403
+ export const CommentReplyButton = (): ReactElement => (
404
+ <button
405
+ className={`inline-flex cursor-pointer items-center gap-1 rounded-md border ${CHIP_BORDER_CLS} bg-white px-2 py-0.5 text-xs font-medium ${TEXT.muted} transition-colors hover:bg-neutral-100 active:translate-y-px dark:bg-neutral-900 dark:hover:bg-neutral-800`}
406
+ data-comment-reply=""
407
+ type="button"
408
+ >
409
+ <Icon className="h-3 w-3" name="lucide:reply" />
410
+ Reply
411
+ </button>
412
+ );
413
+
414
+ /**
415
+ * Full-width strip inserted between code/diff rows to hold a comment
416
+ * thread (`<Comments>`) — GitHub's inline review-thread row: white
417
+ * background, edge-to-edge top border. `bleed` stretches the strip across
418
+ * a parent's `px-4` padding (annotated code blocks sit inside one).
419
+ * `anchor` lands on `data-strip-*`: replies posted into the strip inherit
420
+ * it and the markdown serializer emits it on the new `<Comment>`.
421
+ * `data-comment-strip`/`data-thread-tools` are client-JS hooks.
422
+ */
423
+ export const CommentStrip = (props: {
424
+ /** Anchor replies inherit (file/lines/side). */
425
+ anchor?: { file?: string; lines?: string; side?: "new" | "old" };
426
+ bleed?: boolean;
427
+ children?: ReactNode;
428
+ }): ReactElement => (
429
+ <div
430
+ className={commentStripCls(props.bleed)}
431
+ data-comment-strip=""
432
+ data-strip-file={props.anchor?.file}
433
+ data-strip-lines={props.anchor?.lines}
434
+ data-strip-side={props.anchor?.side === "old" ? "old" : undefined}
435
+ >
436
+ {props.children}
437
+ <div className="mdxr-thread-tools" data-thread-tools="">
438
+ <CommentReplyButton />
439
+ </div>
440
+ </div>
441
+ );
442
+
443
+ /**
444
+ * File/editor link that may not resolve: renders an `<a>` when `href` is
445
+ * set (with `linkTarget`), else a `<span>` — identical classes either way.
446
+ * Link-only utilities (`hover:underline`, `text-inherit`) are inert on the
447
+ * span, so a single `className` covers both branches.
448
+ */
449
+ export const MaybeLink = (props: {
450
+ children?: ReactNode;
451
+ className?: string;
452
+ href?: string;
453
+ }): ReactElement =>
454
+ props.href === undefined ? (
455
+ <span className={props.className}>{props.children}</span>
456
+ ) : (
457
+ <a
458
+ className={props.className}
459
+ href={props.href}
460
+ {...linkTarget(props.href)}
461
+ >
462
+ {props.children}
463
+ </a>
464
+ );
465
+
466
+ /**
467
+ * Trailing note slot inside a `ListRow` — the muted free-text annotation at
468
+ * the end of a row (Files/Deps/Changes/…). Renders nothing without children.
469
+ */
470
+ export const RowNote = (props: {
471
+ children?: ReactNode;
472
+ /** Non-conflicting extras, e.g. margin-trimming `[&>*]:…` selectors. */
473
+ className?: string;
474
+ }): ReactElement | null =>
475
+ props.children === undefined ? null : (
476
+ <span
477
+ className={`min-w-0 flex-1 text-sm ${TEXT.muted} ${props.className ?? ""}`}
478
+ >
479
+ {props.children}
480
+ </span>
481
+ );
482
+
483
+ /**
484
+ * Bordered inline chip shell shared by the reference components —
485
+ * FileRef/SymbolRef/Cmd (via CodeChip), Ref's link chip, Graph's standalone
486
+ * node chip. `font-mono` stays with the callers that want mono content.
487
+ */
488
+ export const CODE_CHIP_CLS = `not-prose mx-0.5 inline-flex items-center gap-1.5 rounded-md border ${CHIP_BORDER_CLS} bg-neutral-100 px-1.5 py-0.5 align-baseline text-[0.85em] ${TEXT.code} dark:bg-neutral-800`;
489
+
490
+ /** Bordered inline code chip — the `<code>` shell used by FileRef/SymbolRef/Cmd. */
491
+ export const CodeChip = (props: { children?: ReactNode }): ReactElement => (
492
+ <code className={`font-mono ${CODE_CHIP_CLS}`}>{props.children}</code>
493
+ );
494
+
495
+ /** `path` plus its `:lines` suffix — the file-location label. */
496
+ export const PathLabel = (props: {
497
+ lines?: string;
498
+ /** Classes for the `:{lines}` suffix (e.g. `opacity-60`); plain text otherwise. */
499
+ linesClassName?: string;
500
+ path: string;
501
+ }): ReactElement => {
502
+ const suffix = nonEmpty(props.lines) ? `:${props.lines}` : "";
503
+ return (
504
+ <>
505
+ {props.path}
506
+ {props.linesClassName === undefined || suffix === "" ? (
507
+ suffix
508
+ ) : (
509
+ <span className={props.linesClassName}>{suffix}</span>
510
+ )}
511
+ </>
512
+ );
513
+ };
514
+
515
+ /**
516
+ * File-location link: `path:lines` in muted mono, linked to the editor when
517
+ * the path resolves (`useFileLink`). The standard companion of `FlowStep`,
518
+ * `TraceFrame`, `Test` rows.
519
+ */
520
+ export const LocLink = (props: {
521
+ /** Overrides the muted-mono look. */
522
+ className?: string;
523
+ /** Explicit link override (the `href` catalog prop). */
524
+ href?: string;
525
+ lines?: string;
526
+ path: string;
527
+ }): ReactElement => {
528
+ const link = useFileLink(props.path, props.lines, props.href);
529
+ return (
530
+ <MaybeLink
531
+ className={props.className ?? `${LOC_CLS} no-underline hover:underline`}
532
+ href={link}
533
+ >
534
+ <PathLabel lines={props.lines} path={props.path} />
535
+ </MaybeLink>
536
+ );
537
+ };
@@ -0,0 +1,173 @@
1
+ import type { ReactElement } from "react";
2
+ import * as v from "valibot";
3
+
4
+ import { defineComponent, flattenChildren, textOf } from "../define.js";
5
+ import { nonEmpty } from "../guards.js";
6
+ import { TITLE_PROP } from "./attrs.js";
7
+ import { ACTION_BUTTON_CLS, CopyFeedback, Section, TrimBody } from "./bits.js";
8
+ import { isEl } from "./children.js";
9
+ import { CHIP_PROPS, ChipRow } from "./chips.js";
10
+ import { Icon } from "./icon.js";
11
+ import { STATUS_ICON_CLS, STATUS_ICONS, STATUS_PROP } from "./status-badge.js";
12
+ import type { Status } from "./status-badge.js";
13
+ import { BORDER_CLS, COUNT_CHIP_CLS, TEXT } from "./tones.js";
14
+
15
+ const DOT: Record<Status, string> = {
16
+ blocked: "bg-red-500",
17
+ doing: "bg-sky-500",
18
+ done: "bg-emerald-500",
19
+ todo: "bg-neutral-400",
20
+ };
21
+
22
+ /**
23
+ * Lane-to-lane move buttons — the keyboard/touch alternative to drag & drop
24
+ * (HTML5 DnD never reaches touch browsers). `data-board-move` is a client-JS
25
+ * hook: doc-events.ts moves the card ±1 lane on click and disables the
26
+ * button at the board's edges via `disabled`.
27
+ */
28
+ const MoveButton = (props: {
29
+ /** `"-1"` previous lane, `"1"` next lane. */
30
+ dir: "-1" | "1";
31
+ }): ReactElement => (
32
+ <button
33
+ aria-label={
34
+ props.dir === "-1" ? "Move to previous lane" : "Move to next lane"
35
+ }
36
+ className="mdxr-move"
37
+ data-board-move={props.dir}
38
+ title={props.dir === "-1" ? "Move left" : "Move right"}
39
+ type="button"
40
+ >
41
+ <Icon
42
+ className="h-3.5 w-3.5"
43
+ name={props.dir === "-1" ? "lucide:chevron-left" : "lucide:chevron-right"}
44
+ />
45
+ </button>
46
+ );
47
+
48
+ export const BoardCard = defineComponent(
49
+ {
50
+ description:
51
+ "カンバンのカード。title は必須。priority/effort/owner/due で既存チップを並べられる。children は補足テキスト。ドラッグまたは両端の矢印ボタンでレーン間を移動できる",
52
+ schema: v.looseObject({
53
+ ...CHIP_PROPS,
54
+ status: STATUS_PROP,
55
+ title: v.string(),
56
+ }),
57
+ },
58
+ ({ title, priority, effort, owner, due, status, children }) => {
59
+ // Plain-text payload for the markdown serializer — element children
60
+ // (links, emphasis) flatten to their visible text.
61
+ const text = children === undefined ? "" : textOf(children).trim();
62
+ return (
63
+ <div
64
+ className={`rounded-md border bg-white p-2.5 shadow-sm dark:bg-neutral-950 ${BORDER_CLS}`}
65
+ data-board-card=""
66
+ data-card-due={due}
67
+ data-card-effort={effort}
68
+ data-card-owner={owner}
69
+ data-card-priority={priority}
70
+ data-card-status={status}
71
+ data-card-text={nonEmpty(text) ? text : undefined}
72
+ data-card-title={title}
73
+ draggable
74
+ >
75
+ <div className="flex items-start gap-1.5">
76
+ <Icon
77
+ className={`mdxr-grip mt-0.5 h-3.5 w-3.5 shrink-0 ${TEXT.ghost}`}
78
+ name="lucide:grip-vertical"
79
+ />
80
+ {status === undefined ? null : (
81
+ <Icon
82
+ className={`mt-0.5 h-3.5 w-3.5 shrink-0 ${STATUS_ICON_CLS[status]}`}
83
+ label={status}
84
+ name={STATUS_ICONS[status]}
85
+ />
86
+ )}
87
+ <div className="min-w-0 flex-1 text-sm leading-snug font-medium">
88
+ {title}
89
+ </div>
90
+ <span className="mdxr-card-moves -mt-0.5 -mr-1 flex shrink-0 items-center">
91
+ <MoveButton dir="-1" />
92
+ <MoveButton dir="1" />
93
+ </span>
94
+ </div>
95
+ <TrimBody className={`mt-1 text-xs ${TEXT.muted}`}>{children}</TrimBody>
96
+ <ChipRow due={due} effort={effort} owner={owner} priority={priority} />
97
+ </div>
98
+ );
99
+ }
100
+ );
101
+
102
+ export const Lane = defineComponent(
103
+ {
104
+ description:
105
+ "カンバンの列。<Board> の子として使う。title は列名、status でヘッダの色点 (todo|doing|done|blocked)。子の <BoardCard> 数がバッジになる。空の列もドロップ先になる",
106
+ schema: v.looseObject({
107
+ status: STATUS_PROP,
108
+ title: v.string(),
109
+ }),
110
+ },
111
+ ({ title, status, children }) => {
112
+ const count = flattenChildren(children).filter((c) =>
113
+ isEl(c, BoardCard)
114
+ ).length;
115
+ return (
116
+ <section
117
+ className="w-64 shrink-0 rounded-xl bg-neutral-100/70 p-2 dark:bg-neutral-900/70"
118
+ data-board-lane=""
119
+ data-lane-status={status}
120
+ data-lane-title={title}
121
+ >
122
+ <header
123
+ className={`flex items-center gap-2 px-1.5 py-1.5 text-xs font-semibold ${TEXT.body}`}
124
+ >
125
+ {status === undefined ? null : (
126
+ <span
127
+ aria-hidden
128
+ className={`h-2 w-2 shrink-0 rounded-full ${DOT[status]}`}
129
+ />
130
+ )}
131
+ <span className="min-w-0 flex-1 truncate">{title}</span>
132
+ <span className={`mdxr-lane-count ${COUNT_CHIP_CLS}`}>{count}</span>
133
+ </header>
134
+ <div className="min-h-8 space-y-2" data-board-cards="">
135
+ {children}
136
+ </div>
137
+ </section>
138
+ );
139
+ }
140
+ );
141
+
142
+ export const Board = defineComponent(
143
+ {
144
+ description:
145
+ "カンバンボードのコンテナ。<Lane> を横に並べる (はみ出しは横スクロール)。カードはドラッグまたは矢印ボタンで移動でき、移動後の状態を <Board> マークアップとしてコピーできる",
146
+ schema: v.looseObject(TITLE_PROP),
147
+ },
148
+ ({ title, children }) => (
149
+ <Section title={title}>
150
+ <div className="mdxr-board" data-board="" data-board-title={title}>
151
+ <div className="not-prose flex items-start gap-3 overflow-x-auto pb-1">
152
+ {children}
153
+ </div>
154
+ <div className="mdxr-board-tools not-prose mt-2 flex items-center justify-between gap-3">
155
+ <span className={`text-xs ${TEXT.faint}`}>
156
+ Drag cards or use the arrow buttons, then copy the updated markup.
157
+ </span>
158
+ <button
159
+ className={ACTION_BUTTON_CLS}
160
+ data-board-copy=""
161
+ type="button"
162
+ >
163
+ <CopyFeedback
164
+ done="Copied"
165
+ icon="lucide:clipboard-list"
166
+ label="Copy markdown"
167
+ />
168
+ </button>
169
+ </div>
170
+ </div>
171
+ </Section>
172
+ )
173
+ );