@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,458 @@
1
+ import { Fragment } from "react";
2
+ import type { ReactElement, ReactNode } from "react";
3
+
4
+ import { nonEmpty } from "../guards.js";
5
+ import {
6
+ AddCommentButton,
7
+ CaptionBar,
8
+ CommentStrip,
9
+ CopyButton,
10
+ MaybeLink,
11
+ Panel,
12
+ } from "./bits.js";
13
+ import type { DiffHl, DiffHlToken, DiffRow, FileDiff } from "./diff-parse.js";
14
+ import { parseDiff } from "./diff-parse.js";
15
+ import { fileIcon } from "./file-icon.js";
16
+ import { useFileLink } from "./file-link.js";
17
+ import { Icon } from "./icon.js";
18
+ import {
19
+ CAPTION_TITLE_CLS,
20
+ LINK_CLS,
21
+ MINI_CHIP_CLS,
22
+ RAIL_BG_CLS,
23
+ TEXT,
24
+ TEXT_SUB,
25
+ TONE,
26
+ TONE_BAND,
27
+ TONE_TEXT,
28
+ } from "./tones.js";
29
+
30
+ export { parseDiff } from "./diff-parse.js";
31
+ export type { FileDiff } from "./diff-parse.js";
32
+
33
+ /**
34
+ * A comment thread anchored to a rendered diff row (`<Comments>`). `range`
35
+ * selects rows by line number on `side` — the thread lands after the last
36
+ * matching row, like a GitHub review comment; a range matching no row (and
37
+ * a `range`-less, file-level comment) drops into the file card's tail.
38
+ */
39
+ export interface DiffCommentSpec {
40
+ /** Path selecting the file card; absent targets the first file. */
41
+ file?: string;
42
+ /** Raw `lines` spec — thread anchors and markdown copy reuse it. */
43
+ lines?: string;
44
+ /** Rendered comment card(s) injected at the anchor. */
45
+ node: ReactNode;
46
+ /** Inclusive line range on `side`; `end` undefined reads "to EOF". */
47
+ range?: { end?: number; start: number };
48
+ /** Side whose line numbers anchor the comment — `new` (default) or `old`. */
49
+ side: "new" | "old";
50
+ }
51
+
52
+ /**
53
+ * Unified-diff rendering. ```diff / ```patch fences route here via `Pre`,
54
+ * so agents emit ordinary patches and get a structured view: per-file
55
+ * headers with editor links, +/- counts and hunks with dual line numbers.
56
+ * Bare `+`/`-` line streams render as a single anonymous file. Parsing
57
+ * lives in `diff-parse.ts`.
58
+ */
59
+
60
+ /** One block of the 5-block change meter. */
61
+ const blockCls = (i: number, green: number, hasChanges: boolean): string => {
62
+ if (i < green) {
63
+ return "bg-emerald-500";
64
+ }
65
+ return hasChanges ? "bg-red-400" : RAIL_BG_CLS;
66
+ };
67
+
68
+ /** GitHub-style 5-block change meter: green share ∝ adds/(adds+dels). */
69
+ const StatBlocks = ({
70
+ adds,
71
+ dels,
72
+ }: {
73
+ adds: number;
74
+ dels: number;
75
+ }): ReactElement => {
76
+ const total = adds + dels;
77
+ const green = total === 0 ? 0 : Math.min(5, Math.round((adds / total) * 5));
78
+ return (
79
+ <span aria-hidden className="inline-flex items-center gap-0.5">
80
+ {Array.from({ length: 5 }, (_, i) => (
81
+ <span
82
+ className={`h-2 w-2 rounded-[2px] ${blockCls(i, green, total > 0)}`}
83
+ key={i}
84
+ />
85
+ ))}
86
+ </span>
87
+ );
88
+ };
89
+
90
+ const KINDS: Record<string, { cls: string; label: string }> = {
91
+ deleted: {
92
+ cls: TONE.red,
93
+ label: "deleted",
94
+ },
95
+ new: {
96
+ cls: TONE.emerald,
97
+ label: "new file",
98
+ },
99
+ renamed: {
100
+ cls: TONE.violet,
101
+ label: "renamed",
102
+ },
103
+ };
104
+
105
+ const fileKind = (f: FileDiff): string | undefined => {
106
+ if (f.oldPath === undefined && f.newPath !== undefined) {
107
+ return "new";
108
+ }
109
+ if (f.newPath === undefined && f.oldPath !== undefined) {
110
+ return "deleted";
111
+ }
112
+ if (f.oldPath !== undefined && f.oldPath !== f.newPath) {
113
+ return "renamed";
114
+ }
115
+ return undefined;
116
+ };
117
+
118
+ const ROW_CLS: Record<DiffRow["kind"], string> = {
119
+ add: "bg-emerald-500/10 dark:bg-emerald-500/15",
120
+ ctx: "",
121
+ del: "bg-red-500/10 dark:bg-red-500/15",
122
+ note: `${TEXT.faint} italic`,
123
+ };
124
+
125
+ const SIGN_CLS: Record<DiffRow["kind"], string> = {
126
+ add: TONE_TEXT.emerald,
127
+ ctx: TEXT.ghost,
128
+ del: TONE_TEXT.red,
129
+ note: TEXT.ghost,
130
+ };
131
+
132
+ const SIGNS: Record<DiffRow["kind"], string> = {
133
+ add: "+",
134
+ ctx: " ",
135
+ del: "−",
136
+ note: " ",
137
+ };
138
+
139
+ /** One line-number gutter cell — empty when the row has no line on this side. */
140
+ const LineNum = ({ n }: { n?: number }): ReactElement => (
141
+ <span className={`px-2 text-right select-none ${TEXT.faint}`}>{n ?? ""}</span>
142
+ );
143
+
144
+ // Style strings repeat across tokens — same grammar color → same declaration.
145
+ const STYLE_CACHE = new Map<string, Record<string, string>>();
146
+
147
+ /** `--shiki-light:#fff;--shiki-dark:#fff` → a React style object. */
148
+ const shikiStyle = (style: string): Record<string, string> => {
149
+ const hit = STYLE_CACHE.get(style);
150
+ if (hit !== undefined) {
151
+ return hit;
152
+ }
153
+ const out: Record<string, string> = {};
154
+ for (const decl of style.split(";")) {
155
+ const i = decl.indexOf(":");
156
+ if (i > 0) {
157
+ out[decl.slice(0, i).trim()] = decl.slice(i + 1).trim();
158
+ }
159
+ }
160
+ STYLE_CACHE.set(style, out);
161
+ return out;
162
+ };
163
+
164
+ /** Row text, or its language-highlighted tokens when rehypeShiki found a
165
+ * grammar for the file (span vars are themed by the `.mdxr-diff-hl` rules). */
166
+ const RowContent = ({
167
+ row,
168
+ toks,
169
+ }: {
170
+ row: DiffRow;
171
+ toks?: DiffHlToken[] | null;
172
+ }): ReactNode => {
173
+ if (toks === undefined || toks === null) {
174
+ return nonEmpty(row.text) ? row.text : " ";
175
+ }
176
+ return toks.map((tok, i) => (
177
+ <span
178
+ className="mdxr-diff-hl"
179
+ key={i}
180
+ style={tok.s === undefined ? undefined : shikiStyle(tok.s)}
181
+ >
182
+ {tok.t}
183
+ </span>
184
+ ));
185
+ };
186
+
187
+ /** A row's add-comment anchor: del rows anchor old-side, add/ctx new-side;
188
+ * note rows and unnumbered (implicit-hunk) rows can't anchor at all. */
189
+ const rowAnchor = (
190
+ row: DiffRow
191
+ ): { line: number; side: "new" | "old" } | undefined => {
192
+ if (row.kind === "del") {
193
+ return row.oldLine === undefined
194
+ ? undefined
195
+ : { line: row.oldLine, side: "old" };
196
+ }
197
+ if (row.kind === "add" || row.kind === "ctx") {
198
+ return row.newLine === undefined
199
+ ? undefined
200
+ : { line: row.newLine, side: "new" };
201
+ }
202
+ return undefined;
203
+ };
204
+
205
+ const DiffRows = ({
206
+ after,
207
+ file,
208
+ hlRows,
209
+ rows,
210
+ }: {
211
+ /** Comment threads keyed by row index — rendered under that row. */
212
+ after?: ReadonlyMap<number, DiffCommentSpec[]>;
213
+ /** Resolved card path — lands on the "+" button and strip anchors. */
214
+ file?: string;
215
+ hlRows?: (DiffHlToken[] | null)[];
216
+ rows: DiffRow[];
217
+ }): ReactElement => (
218
+ <div className="font-mono text-[0.8125rem] leading-5">
219
+ {rows.map((row, i) => {
220
+ const anchor = rowAnchor(row);
221
+ const group = after?.get(i);
222
+ const last = group?.at(-1);
223
+ return (
224
+ <Fragment key={i}>
225
+ <div
226
+ className={`mdxr-drow grid grid-cols-[2.5rem_2.5rem_1.25rem_minmax(0,1fr)] ${ROW_CLS[row.kind]}`}
227
+ data-comment-row=""
228
+ >
229
+ {anchor === undefined ? null : (
230
+ <AddCommentButton
231
+ file={file}
232
+ line={anchor.line}
233
+ side={anchor.side}
234
+ />
235
+ )}
236
+ <LineNum n={row.oldLine} />
237
+ <LineNum n={row.newLine} />
238
+ <span className={`text-center select-none ${SIGN_CLS[row.kind]}`}>
239
+ {SIGNS[row.kind]}
240
+ </span>
241
+ <span
242
+ className={`pr-4 wrap-anywhere whitespace-pre-wrap ${TEXT.code}`}
243
+ >
244
+ <RowContent row={row} toks={hlRows?.[i]} />
245
+ </span>
246
+ </div>
247
+ {group === undefined ? null : (
248
+ <CommentStrip
249
+ anchor={{
250
+ file,
251
+ lines: last?.lines,
252
+ side: last?.side,
253
+ }}
254
+ >
255
+ {group.map((c): ReactNode => c.node)}
256
+ </CommentStrip>
257
+ )}
258
+ </Fragment>
259
+ );
260
+ })}
261
+ </div>
262
+ );
263
+
264
+ /** The last (document-order) row of `file` whose `side` line number falls
265
+ * in `range` — GitHub anchors a range comment at its end. */
266
+ const anchorRow = (
267
+ file: FileDiff,
268
+ side: "new" | "old",
269
+ range: { end?: number; start: number }
270
+ ): { h: number; r: number } | undefined => {
271
+ const { end, start } = range;
272
+ let hit: { h: number; r: number } | undefined;
273
+ for (const [hi, hunk] of file.hunks.entries()) {
274
+ for (const [ri, row] of hunk.rows.entries()) {
275
+ const ln = side === "old" ? row.oldLine : row.newLine;
276
+ if (ln !== undefined && ln >= start && (end === undefined || ln <= end)) {
277
+ hit = { h: hi, r: ri };
278
+ }
279
+ }
280
+ }
281
+ return hit;
282
+ };
283
+
284
+ /**
285
+ * Group `comments` by their anchor row inside `file`: a thread hangs under
286
+ * the last row (document order) whose `side` line number falls in its
287
+ * range. Threads matching no row — plus file-level comments without a
288
+ * range — collect in `tail` for a strip at the card's end.
289
+ */
290
+ const anchorComments = (
291
+ file: FileDiff,
292
+ comments: readonly DiffCommentSpec[]
293
+ ): {
294
+ after: ReadonlyMap<number, DiffCommentSpec[]>[];
295
+ tail: DiffCommentSpec[];
296
+ } => {
297
+ const after = file.hunks.map(() => new Map<number, DiffCommentSpec[]>());
298
+ const tail: DiffCommentSpec[] = [];
299
+ for (const c of comments) {
300
+ const hit =
301
+ c.range === undefined ? undefined : anchorRow(file, c.side, c.range);
302
+ if (hit === undefined) {
303
+ tail.push(c);
304
+ continue;
305
+ }
306
+ const rows = after[hit.h]?.get(hit.r) ?? [];
307
+ rows.push(c);
308
+ after[hit.h]?.set(hit.r, rows);
309
+ }
310
+ return { after, tail };
311
+ };
312
+
313
+ const FileCard = ({
314
+ comments,
315
+ fallbackPath,
316
+ file,
317
+ hl,
318
+ }: {
319
+ comments?: readonly DiffCommentSpec[];
320
+ fallbackPath?: string;
321
+ file: FileDiff;
322
+ hl?: (DiffHlToken[] | null)[][];
323
+ }): ReactElement => {
324
+ const path = file.newPath ?? file.oldPath ?? fallbackPath;
325
+ const link = useFileLink(path);
326
+ const kind = fileKind(file);
327
+ const label =
328
+ file.oldPath !== undefined &&
329
+ file.newPath !== undefined &&
330
+ file.oldPath !== file.newPath
331
+ ? `${file.oldPath} → ${file.newPath}`
332
+ : (path ?? "diff");
333
+ const anchored = anchorComments(file, comments ?? []);
334
+ return (
335
+ <Panel>
336
+ <CaptionBar className={CAPTION_TITLE_CLS}>
337
+ <Icon
338
+ className="h-3.5 w-3.5 shrink-0"
339
+ name={fileIcon(path ?? "x.diff")}
340
+ />
341
+ <MaybeLink
342
+ className={`min-w-0 truncate font-mono ${LINK_CLS}`}
343
+ href={link}
344
+ >
345
+ {label}
346
+ </MaybeLink>
347
+ {kind === undefined ? null : (
348
+ <span className={`${MINI_CHIP_CLS} ${KINDS[kind].cls}`}>
349
+ {KINDS[kind].label}
350
+ </span>
351
+ )}
352
+ <span className="ml-auto flex shrink-0 items-center gap-2">
353
+ <span className="font-mono font-medium">
354
+ <span className={TONE_TEXT.emerald}>+{file.adds}</span>{" "}
355
+ <span className={TONE_TEXT.red}>−{file.dels}</span>
356
+ </span>
357
+ <StatBlocks adds={file.adds} dels={file.dels} />
358
+ <CopyButton copy={file.raw.join("\n")} title="Copy diff" />
359
+ </span>
360
+ </CaptionBar>
361
+ {file.meta.length > 0 ? (
362
+ <div
363
+ className={`border-b border-neutral-100 px-4 py-1.5 font-mono ${TEXT_SUB} dark:border-neutral-800/60 ${TEXT.faint}`}
364
+ >
365
+ {file.meta.map((m, i) => (
366
+ <div className="truncate" key={i}>
367
+ {m}
368
+ </div>
369
+ ))}
370
+ </div>
371
+ ) : null}
372
+ <div className="overflow-x-auto">
373
+ {file.hunks.map((h, i) => (
374
+ <div key={i}>
375
+ {nonEmpty(h.header) ? (
376
+ <div
377
+ className={`px-4 py-1 font-mono ${TEXT_SUB} ${TONE_BAND.sky}`}
378
+ >
379
+ {h.header}
380
+ </div>
381
+ ) : null}
382
+ <DiffRows
383
+ after={anchored.after[i]}
384
+ file={path}
385
+ hlRows={hl?.[i]}
386
+ rows={h.rows}
387
+ />
388
+ </div>
389
+ ))}
390
+ </div>
391
+ {anchored.tail.length === 0 ? null : (
392
+ <CommentStrip anchor={{ file: path }}>
393
+ {anchored.tail.map((c): ReactNode => c.node)}
394
+ </CommentStrip>
395
+ )}
396
+ </Panel>
397
+ );
398
+ };
399
+
400
+ /**
401
+ * Fan `comments` out to file cards: a comment's `file` picks the first card
402
+ * whose old or new path (or the fence's fallback filename) matches; absent
403
+ * or unmatched `file` lands on the first card.
404
+ */
405
+ const commentsPerFile = (
406
+ files: FileDiff[],
407
+ comments: readonly DiffCommentSpec[],
408
+ fallbackPath?: string
409
+ ): (readonly DiffCommentSpec[] | undefined)[] => {
410
+ const perFile: DiffCommentSpec[][] = files.map(() => []);
411
+ for (const c of comments) {
412
+ const idx =
413
+ c.file === undefined
414
+ ? 0
415
+ : files.findIndex(
416
+ (f) =>
417
+ f.newPath === c.file ||
418
+ f.oldPath === c.file ||
419
+ fallbackPath === c.file
420
+ );
421
+ perFile[Math.max(0, idx)]?.push(c);
422
+ }
423
+ return perFile;
424
+ };
425
+
426
+ /** Rendered by `Pre` for ```diff / ```patch fences. `hl` is the per-row
427
+ * language highlighting parsed from the fence's `data-diffhl` (absent when
428
+ * no grammar was resolved at compile time). */
429
+ export const DiffView = ({
430
+ comments,
431
+ filename,
432
+ hl,
433
+ text,
434
+ }: {
435
+ comments?: readonly DiffCommentSpec[];
436
+ filename?: string;
437
+ hl?: DiffHl;
438
+ text: string;
439
+ }): ReactElement | null => {
440
+ const files = parseDiff(text).filter((f) => f.raw.length > 0);
441
+ if (files.length === 0) {
442
+ return null;
443
+ }
444
+ const perFile = commentsPerFile(files, comments ?? [], filename);
445
+ return (
446
+ <>
447
+ {files.map((f, i) => (
448
+ <FileCard
449
+ comments={perFile[i]}
450
+ fallbackPath={files.length === 1 ? filename : undefined}
451
+ file={f}
452
+ hl={hl?.[i]}
453
+ key={i}
454
+ />
455
+ ))}
456
+ </>
457
+ );
458
+ };
@@ -0,0 +1,58 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { numOf, NUMISH } from "./attrs.js";
5
+ import { Icon } from "./icon.js";
6
+ import { TEXT, TONE_TEXT } from "./tones.js";
7
+
8
+ export const DiffStat = defineComponent(
9
+ {
10
+ description:
11
+ 'diff 量サマリ (PR ヘッダーの "+N −M across F files")。files/adds/dels は数値。追加=緑・削除=赤の proportional バー付き。インラインでも単独でも使える',
12
+ schema: v.looseObject({
13
+ adds: v.optional(NUMISH, "0"),
14
+ dels: v.optional(NUMISH, "0"),
15
+ files: v.optional(NUMISH),
16
+ }),
17
+ },
18
+ ({ files, adds, dels }) => {
19
+ const a = numOf(adds) ?? 0;
20
+ const d = numOf(dels) ?? 0;
21
+ const f = numOf(files);
22
+ const total = a + d;
23
+ const aPct = total > 0 ? (a / total) * 100 : 50;
24
+ return (
25
+ <span className="not-prose mx-0.5 inline-flex items-center gap-2 align-baseline text-xs">
26
+ {f === undefined ? null : (
27
+ <span className={`inline-flex items-center gap-1 ${TEXT.muted}`}>
28
+ <Icon className="h-3.5 w-3.5" name="lucide:file-diff" />
29
+ {f} {f === 1 ? "file" : "files"}
30
+ </span>
31
+ )}
32
+ <span
33
+ className={`font-mono font-medium tabular-nums ${TONE_TEXT.emerald}`}
34
+ >
35
+ +{a}
36
+ </span>
37
+ <span className={`font-mono font-medium tabular-nums ${TONE_TEXT.red}`}>
38
+ −{d}
39
+ </span>
40
+ {total > 0 ? (
41
+ <span
42
+ aria-hidden
43
+ className="inline-flex h-2 w-14 overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-700"
44
+ >
45
+ <span
46
+ className="h-full bg-emerald-500"
47
+ style={{ width: `${aPct}%` }}
48
+ />
49
+ <span
50
+ className="h-full bg-red-500"
51
+ style={{ width: `${100 - aPct}%` }}
52
+ />
53
+ </span>
54
+ ) : null}
55
+ </span>
56
+ );
57
+ }
58
+ );
package/src/ui/due.tsx ADDED
@@ -0,0 +1,65 @@
1
+ import { differenceInCalendarDays, isValid, parseISO } from "date-fns";
2
+ import { useContext } from "react";
3
+ import * as v from "valibot";
4
+
5
+ import { defineComponent } from "../define.js";
6
+ import { DocContext } from "../doc-context.js";
7
+ import { nonEmpty } from "../guards.js";
8
+ import { Icon } from "./icon.js";
9
+ import { TONE } from "./tones.js";
10
+
11
+ const CLASSES = {
12
+ neutral: TONE.neutral,
13
+ overdue: TONE.red,
14
+ soon: TONE.amber,
15
+ };
16
+
17
+ const dueState = (
18
+ date: string,
19
+ now: Date
20
+ ): { cls: string; rel: string | null } => {
21
+ const d = parseISO(date);
22
+ if (!isValid(d)) {
23
+ return { cls: CLASSES.neutral, rel: null };
24
+ }
25
+ const days = differenceInCalendarDays(d, now);
26
+ if (days < 0) {
27
+ return { cls: CLASSES.overdue, rel: `${-days}d overdue` };
28
+ }
29
+ if (days === 0) {
30
+ return { cls: CLASSES.soon, rel: "today" };
31
+ }
32
+ if (days <= 3) {
33
+ return { cls: CLASSES.soon, rel: `in ${days}d` };
34
+ }
35
+ return { cls: CLASSES.neutral, rel: `in ${days}d` };
36
+ };
37
+
38
+ export const Due = defineComponent(
39
+ {
40
+ description:
41
+ "期限チップ。date 属性 (ISO) から urgency を描画時点で計算する",
42
+ schema: v.looseObject({
43
+ date: v.string(),
44
+ label: v.optional(v.string()),
45
+ }),
46
+ },
47
+ ({ date, label }) => {
48
+ // ctx.now is the SSR timestamp replayed by the hydration bundle; the
49
+ // fallback covers standalone use (Storybook, tests).
50
+ const { now } = useContext(DocContext);
51
+ const { cls, rel } = dueState(date, now ?? new Date());
52
+ return (
53
+ <span
54
+ className={`not-prose inline-flex items-baseline gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${cls}`}
55
+ >
56
+ <Icon className="h-3 w-3 self-center" name="lucide:calendar-days" />
57
+ {nonEmpty(label) ? <span className="opacity-75">{label}</span> : null}
58
+ <time dateTime={date} className="font-mono">
59
+ {date}
60
+ </time>
61
+ {rel === null ? null : <span className="opacity-60">{rel}</span>}
62
+ </span>
63
+ );
64
+ }
65
+ );
@@ -0,0 +1,29 @@
1
+ import * as v from "valibot";
2
+
3
+ import { defineComponent } from "../define.js";
4
+ import { Icon } from "./icon.js";
5
+ import { TONE } from "./tones.js";
6
+
7
+ export const EFFORT_SIZES = ["xs", "s", "m", "l", "xl"] as const;
8
+ export type EffortSize = (typeof EFFORT_SIZES)[number];
9
+
10
+ export const Effort = defineComponent(
11
+ {
12
+ description:
13
+ '工数ピル。size は xs|s|m|l|xl。children に "3d" のような見積もりを書ける',
14
+ schema: v.looseObject({
15
+ size: v.optional(v.picklist(EFFORT_SIZES), "m"),
16
+ }),
17
+ },
18
+ ({ size, children }) => (
19
+ <span
20
+ className={`not-prose inline-flex items-baseline gap-1 rounded-full px-2 py-0.5 font-mono text-xs font-medium ${TONE.neutral}`}
21
+ >
22
+ <Icon className="h-3 w-3 self-center" name="lucide:hourglass" />
23
+ {size.toUpperCase()}
24
+ {children === undefined || children === "" ? null : (
25
+ <span className="font-sans font-normal opacity-75">{children}</span>
26
+ )}
27
+ </span>
28
+ )
29
+ );