@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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2902 -0
- package/dist/components.d.mts +791 -0
- package/dist/components.mjs +2 -0
- package/dist/config-SI9IyFiC.mjs +184 -0
- package/dist/doc-context-CEqzMYKv.d.mts +568 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +4 -0
- package/dist/ui-GKFD0mx5.mjs +15986 -0
- package/package.json +131 -0
- package/skill/SKILL.md +49 -0
- package/skill/references/components/charts.md +172 -0
- package/skill/references/components/document.md +98 -0
- package/skill/references/components/forms.md +35 -0
- package/skill/references/components/investigation.md +198 -0
- package/skill/references/components/layout.md +68 -0
- package/skill/references/components/output.md +158 -0
- package/skill/references/components/planning.md +155 -0
- package/skill/references/components/reports.md +252 -0
- package/skill/references/components/shadcn.md +19 -0
- package/skill/references/components.md +186 -0
- package/skill/references/extending.md +48 -0
- package/src/ask-sheet.ts +40 -0
- package/src/assets/css.ts +392 -0
- package/src/assets/scripts.ts +78 -0
- package/src/catalog.ts +287 -0
- package/src/cli.ts +178 -0
- package/src/client/doc-events.ts +1108 -0
- package/src/client/entry.ts +27 -0
- package/src/client-js.ts +47 -0
- package/src/component-map.ts +47 -0
- package/src/components/ui/accordion.tsx +77 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +208 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +121 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +88 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +226 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/carousel.tsx +246 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/combobox.tsx +298 -0
- package/src/components/ui/command.tsx +193 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +4 -0
- package/src/components/ui/drawer.tsx +227 -0
- package/src/components/ui/dropdown-menu.tsx +269 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +237 -0
- package/src/components/ui/fieldset.tsx +32 -0
- package/src/components/ui/frame.tsx +87 -0
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +159 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +202 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/menubar.tsx +284 -0
- package/src/components/ui/message-scroller.tsx +128 -0
- package/src/components/ui/message.tsx +91 -0
- package/src/components/ui/meter.tsx +80 -0
- package/src/components/ui/native-select.tsx +64 -0
- package/src/components/ui/navigation-menu.tsx +170 -0
- package/src/components/ui/pagination.tsx +133 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/questionnaire.tsx +328 -0
- package/src/components/ui/radio-group.tsx +35 -0
- package/src/components/ui/resizable.tsx +49 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +201 -0
- package/src/components/ui/separator.tsx +22 -0
- package/src/components/ui/sheet.tsx +135 -0
- package/src/components/ui/sidebar.tsx +730 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +113 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toast.tsx +229 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/components.ts +78 -0
- package/src/config.ts +56 -0
- package/src/define.ts +122 -0
- package/src/doc-context.ts +20 -0
- package/src/editor.ts +94 -0
- package/src/format-error.ts +78 -0
- package/src/guards.ts +84 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/html.ts +91 -0
- package/src/hydrate/export-index.ts +232 -0
- package/src/hydrate/import-scan.ts +169 -0
- package/src/hydrate/plugins.ts +118 -0
- package/src/hydrate/runtime-module.ts +145 -0
- package/src/hydrate-runtime.ts +67 -0
- package/src/hydrate.ts +147 -0
- package/src/index.ts +16 -0
- package/src/init.ts +57 -0
- package/src/langs.ts +148 -0
- package/src/lines.ts +53 -0
- package/src/load-user-module.ts +191 -0
- package/src/mdx.ts +248 -0
- package/src/paths.ts +36 -0
- package/src/rehype/shiki.ts +533 -0
- package/src/remark/alerts.ts +53 -0
- package/src/remark/ast.ts +96 -0
- package/src/remark/callouts.ts +25 -0
- package/src/remark/code-file.ts +85 -0
- package/src/remark/code-meta.ts +21 -0
- package/src/remark/directives.ts +170 -0
- package/src/remark/file-paths.ts +64 -0
- package/src/remark/headings.ts +131 -0
- package/src/remark/no-js.ts +40 -0
- package/src/render.ts +337 -0
- package/src/serve.ts +322 -0
- package/src/styles/globals.css +134 -0
- package/src/styles/shadcn.css +641 -0
- package/src/tailwind.ts +119 -0
- package/src/ui/approvals.tsx +76 -0
- package/src/ui/ask-question.tsx +386 -0
- package/src/ui/ask.tsx +206 -0
- package/src/ui/attrs.ts +51 -0
- package/src/ui/audit.tsx +139 -0
- package/src/ui/bar-chart.tsx +334 -0
- package/src/ui/benchmarks.tsx +143 -0
- package/src/ui/bits.tsx +537 -0
- package/src/ui/board.tsx +173 -0
- package/src/ui/bridge.tsx +207 -0
- package/src/ui/bumps.tsx +178 -0
- package/src/ui/callout.tsx +106 -0
- package/src/ui/changes.tsx +89 -0
- package/src/ui/chart-bits.tsx +52 -0
- package/src/ui/chart.ts +577 -0
- package/src/ui/checks.tsx +203 -0
- package/src/ui/child-index.tsx +44 -0
- package/src/ui/children.ts +43 -0
- package/src/ui/chips.tsx +49 -0
- package/src/ui/cmd.tsx +27 -0
- package/src/ui/columns.tsx +31 -0
- package/src/ui/comments.tsx +544 -0
- package/src/ui/compare.tsx +67 -0
- package/src/ui/decision.tsx +79 -0
- package/src/ui/deps.tsx +78 -0
- package/src/ui/details.tsx +43 -0
- package/src/ui/diff-parse.ts +307 -0
- package/src/ui/diff.tsx +458 -0
- package/src/ui/diffstat.tsx +58 -0
- package/src/ui/due.tsx +65 -0
- package/src/ui/effort.tsx +29 -0
- package/src/ui/endpoints.tsx +118 -0
- package/src/ui/envvars.tsx +96 -0
- package/src/ui/figure.tsx +37 -0
- package/src/ui/file-icon.ts +1020 -0
- package/src/ui/file-link.ts +32 -0
- package/src/ui/file-ref.tsx +39 -0
- package/src/ui/files.tsx +112 -0
- package/src/ui/findings.tsx +85 -0
- package/src/ui/flow.tsx +73 -0
- package/src/ui/funnel.tsx +121 -0
- package/src/ui/gantt.tsx +443 -0
- package/src/ui/gauges.tsx +135 -0
- package/src/ui/glossary.tsx +29 -0
- package/src/ui/graph-layout.ts +149 -0
- package/src/ui/graph-specs.tsx +102 -0
- package/src/ui/graph.tsx +278 -0
- package/src/ui/grid.tsx +119 -0
- package/src/ui/hypothesis.tsx +94 -0
- package/src/ui/icon.tsx +80 -0
- package/src/ui/incident.tsx +130 -0
- package/src/ui/index.ts +342 -0
- package/src/ui/ins-del.tsx +41 -0
- package/src/ui/json.tsx +190 -0
- package/src/ui/layout.ts +9 -0
- package/src/ui/line-chart.tsx +251 -0
- package/src/ui/matrix.tsx +208 -0
- package/src/ui/meta.tsx +73 -0
- package/src/ui/option.tsx +64 -0
- package/src/ui/owner.tsx +40 -0
- package/src/ui/packages.tsx +104 -0
- package/src/ui/pathway.tsx +96 -0
- package/src/ui/phase.tsx +41 -0
- package/src/ui/pie-chart.tsx +170 -0
- package/src/ui/plan.tsx +77 -0
- package/src/ui/pre.tsx +175 -0
- package/src/ui/priority.tsx +51 -0
- package/src/ui/props.tsx +77 -0
- package/src/ui/quadrant.tsx +172 -0
- package/src/ui/radar.tsx +200 -0
- package/src/ui/ref.tsx +160 -0
- package/src/ui/release.tsx +178 -0
- package/src/ui/req.tsx +42 -0
- package/src/ui/review.tsx +133 -0
- package/src/ui/risk.tsx +67 -0
- package/src/ui/sankey.tsx +287 -0
- package/src/ui/scatter.tsx +237 -0
- package/src/ui/schema.tsx +113 -0
- package/src/ui/score.tsx +105 -0
- package/src/ui/search.tsx +120 -0
- package/src/ui/series.tsx +38 -0
- package/src/ui/severity.tsx +69 -0
- package/src/ui/shadcn.tsx +216 -0
- package/src/ui/spark.tsx +86 -0
- package/src/ui/stack.tsx +57 -0
- package/src/ui/stats.tsx +63 -0
- package/src/ui/status-badge.tsx +66 -0
- package/src/ui/statuspage.tsx +238 -0
- package/src/ui/steps.tsx +74 -0
- package/src/ui/summary.tsx +41 -0
- package/src/ui/symbol-ref.tsx +73 -0
- package/src/ui/terminal.tsx +117 -0
- package/src/ui/tests.tsx +218 -0
- package/src/ui/timeline.tsx +63 -0
- package/src/ui/toc.tsx +56 -0
- package/src/ui/tones.ts +187 -0
- package/src/ui/trace.tsx +69 -0
- package/src/ui/tree.tsx +281 -0
- package/src/ui/treemap.tsx +128 -0
- package/src/ui/venn.tsx +258 -0
- package/src/ui/verdict.tsx +78 -0
- package/src/ui/waterfall.tsx +142 -0
package/src/ui/diff.tsx
ADDED
|
@@ -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
|
+
);
|