@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
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { Fragment, isValidElement } from "react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
defineComponent,
|
|
7
|
+
flattenChildren,
|
|
8
|
+
parseProps,
|
|
9
|
+
textOf,
|
|
10
|
+
} from "../define.js";
|
|
11
|
+
import { asString, isRecord, nonEmpty } from "../guards.js";
|
|
12
|
+
import { fenceFilename, parseLineRange } from "../lines.js";
|
|
13
|
+
import {
|
|
14
|
+
ACTION_BUTTON_CLS,
|
|
15
|
+
AddCommentButton,
|
|
16
|
+
CommentReplyButton,
|
|
17
|
+
CommentStrip,
|
|
18
|
+
CopyFeedback,
|
|
19
|
+
MaybeLink,
|
|
20
|
+
Panel,
|
|
21
|
+
TrimBody,
|
|
22
|
+
} from "./bits.js";
|
|
23
|
+
import { indexChildren } from "./child-index.js";
|
|
24
|
+
import { isEl, propOf } from "./children.js";
|
|
25
|
+
import { parseDiff, parseDiffHl } from "./diff-parse.js";
|
|
26
|
+
import type { DiffCommentSpec } from "./diff.js";
|
|
27
|
+
import { DiffView } from "./diff.js";
|
|
28
|
+
import { useFileLink } from "./file-link.js";
|
|
29
|
+
import { Icon } from "./icon.js";
|
|
30
|
+
import { initials } from "./owner.js";
|
|
31
|
+
import { CodeHeader, Pre } from "./pre.js";
|
|
32
|
+
import { Comment } from "./review.js";
|
|
33
|
+
import type { SeverityLevel } from "./severity.js";
|
|
34
|
+
import { SEVERITY_LEVELS, Severity } from "./severity.js";
|
|
35
|
+
import {
|
|
36
|
+
BORDER_CLS,
|
|
37
|
+
CHIP_BORDER_CLS,
|
|
38
|
+
commentStripCls,
|
|
39
|
+
LINK_CLS,
|
|
40
|
+
LOC_CLS,
|
|
41
|
+
MINI_CHIP_CLS,
|
|
42
|
+
SURFACE_CLS,
|
|
43
|
+
TEXT,
|
|
44
|
+
TONE,
|
|
45
|
+
TRIM_CLS,
|
|
46
|
+
} from "./tones.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Line-anchored comment threads on a code or diff fence — GitHub's PR
|
|
50
|
+
* review shape. `<Comments>` takes one fenced code block child plus
|
|
51
|
+
* `<Comment>` children carrying `lines`/`side`/`file` anchors; each thread
|
|
52
|
+
* renders inline under the line it targets. Anchoring and file/hunk
|
|
53
|
+
* routing live here and in diff.tsx's `DiffCommentSpec` plumbing.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const SIDES = ["old", "new"] as const;
|
|
57
|
+
type Side = (typeof SIDES)[number];
|
|
58
|
+
|
|
59
|
+
/** `<Comment>` props as read by `<Comments>` — the standalone schema is
|
|
60
|
+
* `looseObject`, so anchor attributes pass through; this schema validates
|
|
61
|
+
* them (incl. the `lines` grammar) for the anchored context. */
|
|
62
|
+
const ANNOTATION_SCHEMA = v.looseObject({
|
|
63
|
+
author: v.optional(v.string()),
|
|
64
|
+
file: v.optional(v.string()),
|
|
65
|
+
href: v.optional(v.string()),
|
|
66
|
+
lines: v.optional(
|
|
67
|
+
v.pipe(
|
|
68
|
+
v.string(),
|
|
69
|
+
v.check(
|
|
70
|
+
(s) => parseLineRange(s) !== undefined,
|
|
71
|
+
'expected "40", "40-52" or "40-"'
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
),
|
|
75
|
+
severity: v.optional(v.picklist(SEVERITY_LEVELS)),
|
|
76
|
+
side: v.optional(v.picklist(SIDES), "new"),
|
|
77
|
+
title: v.optional(v.string()),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
interface Annotation {
|
|
81
|
+
file?: string;
|
|
82
|
+
/** Raw `lines` spec — strip anchors and the markdown serializer reuse it. */
|
|
83
|
+
lines?: string;
|
|
84
|
+
/** Rendered card — injected at the anchor row. */
|
|
85
|
+
node: ReactNode;
|
|
86
|
+
range?: { end?: number; start: number };
|
|
87
|
+
side: Side;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isLineElement = (n: ReactElement): boolean =>
|
|
91
|
+
/(?:^|\s)line(?:\s|$)/u.test(asString(propOf(n, "className")) ?? "");
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One rendered node per source line of a `code` element. Shiki output
|
|
95
|
+
* already arrives as `span.line` children (separated by `\n` text nodes);
|
|
96
|
+
* unhighlighted fences carry raw text, split here into matching `line`
|
|
97
|
+
* spans so numbering/bands treat every row identically.
|
|
98
|
+
*/
|
|
99
|
+
const lineNodes = (children: ReactNode): ReactNode[] => {
|
|
100
|
+
const lines = flattenChildren(children).filter(
|
|
101
|
+
(n): n is ReactElement => isValidElement(n) && isLineElement(n)
|
|
102
|
+
);
|
|
103
|
+
if (lines.length > 0) {
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
const text = textOf(children).replace(/\n$/u, "");
|
|
107
|
+
return text === ""
|
|
108
|
+
? []
|
|
109
|
+
: text.split("\n").map((t, i) => (
|
|
110
|
+
<span className="line" key={i}>
|
|
111
|
+
{t}
|
|
112
|
+
</span>
|
|
113
|
+
));
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Avatar disc in the strip's left gutter — initials for a named author,
|
|
117
|
+
* a thread icon otherwise (GitHub always shows the commenter's avatar). */
|
|
118
|
+
const Avatar = ({ author }: { author?: string }): ReactElement =>
|
|
119
|
+
nonEmpty(author) ? (
|
|
120
|
+
<span
|
|
121
|
+
aria-hidden
|
|
122
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-[10px] font-semibold text-neutral-700 dark:bg-neutral-600 dark:text-neutral-100"
|
|
123
|
+
>
|
|
124
|
+
{initials(author)}
|
|
125
|
+
</span>
|
|
126
|
+
) : (
|
|
127
|
+
<span
|
|
128
|
+
aria-hidden
|
|
129
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-neutral-500 dark:bg-neutral-700 dark:text-neutral-400"
|
|
130
|
+
>
|
|
131
|
+
<Icon className="h-3.5 w-3.5" name="lucide:message-square-text" />
|
|
132
|
+
</span>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
/** One GitHub-style review comment: avatar in the left gutter, then a
|
|
136
|
+
* bordered box whose muted header reads `name commented · title` with a
|
|
137
|
+
* right cluster of severity pill, `old`-side chip and the `:lines` anchor
|
|
138
|
+
* link. `children` is the MDX body — nested fences render as ordinary
|
|
139
|
+
* code panels, so ` ```diff ` suggestion blocks just work. */
|
|
140
|
+
const CommentCard = ({
|
|
141
|
+
author,
|
|
142
|
+
children,
|
|
143
|
+
file,
|
|
144
|
+
href,
|
|
145
|
+
lines,
|
|
146
|
+
path,
|
|
147
|
+
severity,
|
|
148
|
+
side,
|
|
149
|
+
text,
|
|
150
|
+
title,
|
|
151
|
+
}: {
|
|
152
|
+
author?: string;
|
|
153
|
+
children?: ReactNode;
|
|
154
|
+
/** Authored `file` attr (not the resolved link path) — serialized back. */
|
|
155
|
+
file?: string;
|
|
156
|
+
href?: string;
|
|
157
|
+
lines?: string;
|
|
158
|
+
path?: string;
|
|
159
|
+
severity?: SeverityLevel;
|
|
160
|
+
side?: Side;
|
|
161
|
+
/** Flattened body text — the markdown serializer's `<Comment>` payload. */
|
|
162
|
+
text?: string;
|
|
163
|
+
title?: string;
|
|
164
|
+
}): ReactElement => {
|
|
165
|
+
const link = useFileLink(path, lines, href);
|
|
166
|
+
const name = nonEmpty(author) ? author.replace(/^@+/u, "") : undefined;
|
|
167
|
+
// data-mdxr-comment marks the card for the markdown serializer; each
|
|
168
|
+
// data-comment-* attr maps back to a <Comment> attribute (doc-events.ts).
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
className="flex gap-2.5"
|
|
172
|
+
data-comment-author={author}
|
|
173
|
+
data-comment-file={file}
|
|
174
|
+
data-comment-href={href}
|
|
175
|
+
data-comment-lines={lines}
|
|
176
|
+
data-comment-severity={severity}
|
|
177
|
+
data-comment-side={side === "old" ? "old" : undefined}
|
|
178
|
+
data-comment-text={text}
|
|
179
|
+
data-comment-title={title}
|
|
180
|
+
data-mdxr-comment=""
|
|
181
|
+
>
|
|
182
|
+
<Avatar author={author} />
|
|
183
|
+
<article
|
|
184
|
+
className={`min-w-0 flex-1 overflow-hidden rounded-md border ${BORDER_CLS} bg-white dark:bg-neutral-950`}
|
|
185
|
+
>
|
|
186
|
+
<header
|
|
187
|
+
className={`flex flex-wrap items-center gap-x-1.5 gap-y-1 border-b ${BORDER_CLS} ${SURFACE_CLS} px-3 py-1.5 text-xs`}
|
|
188
|
+
>
|
|
189
|
+
{name === undefined ? null : (
|
|
190
|
+
<span className={`font-semibold ${TEXT.strong}`}>{name}</span>
|
|
191
|
+
)}
|
|
192
|
+
<span className={TEXT.muted}>commented</span>
|
|
193
|
+
{nonEmpty(title) ? (
|
|
194
|
+
<span className={`font-medium ${TEXT.strong}`}>· {title}</span>
|
|
195
|
+
) : null}
|
|
196
|
+
<span className="ml-auto inline-flex items-center gap-1.5">
|
|
197
|
+
{severity === undefined ? null : <Severity level={severity} />}
|
|
198
|
+
{side === "old" ? (
|
|
199
|
+
<span className={`${MINI_CHIP_CLS} ${TONE.red}`}>old</span>
|
|
200
|
+
) : null}
|
|
201
|
+
{nonEmpty(lines) ? (
|
|
202
|
+
<MaybeLink className={`${LOC_CLS} ${LINK_CLS}`} href={link}>
|
|
203
|
+
:{lines}
|
|
204
|
+
</MaybeLink>
|
|
205
|
+
) : null}
|
|
206
|
+
</span>
|
|
207
|
+
</header>
|
|
208
|
+
<TrimBody className="px-3.5 py-2.5 text-sm">{children}</TrimBody>
|
|
209
|
+
</article>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Typed `props.children` off an element — `isValidElement` narrowing
|
|
215
|
+
* keeps this free of type assertions. */
|
|
216
|
+
const childrenOf = (el: unknown): ReactNode =>
|
|
217
|
+
isValidElement<{ children?: ReactNode }>(el) ? el.props.children : undefined;
|
|
218
|
+
|
|
219
|
+
/** Raw `<Comment>` element → annotation record with its rendered card. */
|
|
220
|
+
const toAnnotation = (
|
|
221
|
+
el: ReactElement,
|
|
222
|
+
i: number,
|
|
223
|
+
fallbackPath: string | undefined
|
|
224
|
+
): Annotation => {
|
|
225
|
+
const p = parseProps(ANNOTATION_SCHEMA, el.props, "Comment");
|
|
226
|
+
const range = p.lines === undefined ? undefined : parseLineRange(p.lines);
|
|
227
|
+
const node = (
|
|
228
|
+
<CommentCard
|
|
229
|
+
author={p.author}
|
|
230
|
+
file={p.file}
|
|
231
|
+
href={p.href}
|
|
232
|
+
key={i}
|
|
233
|
+
lines={p.lines}
|
|
234
|
+
path={p.file ?? fallbackPath}
|
|
235
|
+
severity={p.severity}
|
|
236
|
+
side={p.side}
|
|
237
|
+
text={textOf(childrenOf(el)).trim()}
|
|
238
|
+
title={p.title}
|
|
239
|
+
>
|
|
240
|
+
{childrenOf(el)}
|
|
241
|
+
</CommentCard>
|
|
242
|
+
);
|
|
243
|
+
return { file: p.file, lines: p.lines, node, range, side: p.side };
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** Group comments by anchor line: a thread lands under `min(end, lines)`
|
|
247
|
+
* — comments starting past EOF join the file-level `tail`. */
|
|
248
|
+
const anchorCode = (
|
|
249
|
+
comments: readonly Annotation[],
|
|
250
|
+
lineCount: number
|
|
251
|
+
): { after: ReadonlyMap<number, Annotation[]>; tail: Annotation[] } => {
|
|
252
|
+
const after = new Map<number, Annotation[]>();
|
|
253
|
+
const tail: Annotation[] = [];
|
|
254
|
+
for (const c of comments) {
|
|
255
|
+
if (c.range === undefined || c.range.start > lineCount) {
|
|
256
|
+
tail.push(c);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const end = Math.min(c.range.end ?? lineCount, lineCount);
|
|
260
|
+
after.set(end, [...(after.get(end) ?? []), c]);
|
|
261
|
+
}
|
|
262
|
+
return { after, tail };
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/** The anchor replies inherit — the thread's last comment's spec. */
|
|
266
|
+
const stripAnchor = (
|
|
267
|
+
group: Annotation[] | undefined
|
|
268
|
+
): { file?: string; lines?: string; side?: Side } | undefined => {
|
|
269
|
+
const last = group?.at(-1);
|
|
270
|
+
return last === undefined
|
|
271
|
+
? undefined
|
|
272
|
+
: { file: last.file, lines: last.lines, side: last.side };
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/** The fenced subject decomposed: `code` element attributes + raw text. */
|
|
276
|
+
interface SubjectBlock {
|
|
277
|
+
children: ReactNode;
|
|
278
|
+
className?: string;
|
|
279
|
+
diffhl?: string;
|
|
280
|
+
filename?: string;
|
|
281
|
+
lang?: string;
|
|
282
|
+
/** Raw fence meta — the markdown serializer reproduces it verbatim. */
|
|
283
|
+
meta: string;
|
|
284
|
+
text: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const subjectBlock = (subject: ReactElement): SubjectBlock => {
|
|
288
|
+
const codeEl = flattenChildren(childrenOf(subject)).find(
|
|
289
|
+
(n): n is ReactElement => isValidElement(n) && n.type === "code"
|
|
290
|
+
);
|
|
291
|
+
const codeProps = isRecord(codeEl?.props) ? codeEl.props : {};
|
|
292
|
+
const lang = /language-(?<lang>[\w-]+)/u.exec(
|
|
293
|
+
asString(codeProps.className) ?? ""
|
|
294
|
+
)?.groups?.lang;
|
|
295
|
+
const meta =
|
|
296
|
+
asString(propOf(subject, "meta")) ??
|
|
297
|
+
asString(codeProps.meta) ??
|
|
298
|
+
asString(codeProps.metastring) ??
|
|
299
|
+
"";
|
|
300
|
+
const children = childrenOf(codeEl);
|
|
301
|
+
return {
|
|
302
|
+
children,
|
|
303
|
+
className: asString(codeProps.className),
|
|
304
|
+
diffhl: asString(codeProps["data-diffhl"]),
|
|
305
|
+
filename: fenceFilename(meta),
|
|
306
|
+
lang,
|
|
307
|
+
meta,
|
|
308
|
+
text: textOf(children),
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const isDiffBlock = (block: SubjectBlock): boolean =>
|
|
313
|
+
(block.lang === "diff" || block.lang === "patch") &&
|
|
314
|
+
block.text.trim() !== "" &&
|
|
315
|
+
parseDiff(block.text).some((f) => f.raw.length > 0);
|
|
316
|
+
|
|
317
|
+
/** Code-block subject: `.line` rows with the comment strips interleaved —
|
|
318
|
+
* the `.shiki` ancestor keeps token colors and the `has-line-numbers`
|
|
319
|
+
* counter numbers rows across strip boundaries. */
|
|
320
|
+
const AnnotatedCode = ({
|
|
321
|
+
block,
|
|
322
|
+
comments,
|
|
323
|
+
}: {
|
|
324
|
+
block: SubjectBlock;
|
|
325
|
+
comments: readonly Annotation[];
|
|
326
|
+
}): ReactElement => {
|
|
327
|
+
const lines = lineNodes(block.children);
|
|
328
|
+
const anchored = anchorCode(comments, lines.length);
|
|
329
|
+
// `language-*`/`has-*` classes off the code element keep feature flags
|
|
330
|
+
// (focused lines, diff markers); `shiki`/`has-line-numbers` come back
|
|
331
|
+
// canonically so the class list never duplicates.
|
|
332
|
+
const codeCls = (block.className ?? "")
|
|
333
|
+
.split(/\s+/u)
|
|
334
|
+
.filter((c) => c !== "" && c !== "shiki" && c !== "has-line-numbers")
|
|
335
|
+
.join(" ");
|
|
336
|
+
return (
|
|
337
|
+
<Panel>
|
|
338
|
+
<CodeHeader
|
|
339
|
+
filename={block.filename}
|
|
340
|
+
lang={block.lang}
|
|
341
|
+
text={block.text}
|
|
342
|
+
/>
|
|
343
|
+
<div className="overflow-x-auto bg-white py-2 dark:bg-neutral-950">
|
|
344
|
+
<div
|
|
345
|
+
className={`${codeCls} shiki has-line-numbers px-4 font-mono text-[0.8125rem] leading-5 whitespace-pre`}
|
|
346
|
+
>
|
|
347
|
+
{lines.map((el, i) => (
|
|
348
|
+
<Fragment key={i}>
|
|
349
|
+
<div className="mdxr-cline" data-comment-row="">
|
|
350
|
+
<AddCommentButton line={i + 1} />
|
|
351
|
+
{el}
|
|
352
|
+
</div>
|
|
353
|
+
{anchored.after.get(i + 1) === undefined ? null : (
|
|
354
|
+
<CommentStrip
|
|
355
|
+
anchor={stripAnchor(anchored.after.get(i + 1))}
|
|
356
|
+
bleed
|
|
357
|
+
>
|
|
358
|
+
{anchored.after.get(i + 1)?.map((a): ReactNode => a.node)}
|
|
359
|
+
</CommentStrip>
|
|
360
|
+
)}
|
|
361
|
+
</Fragment>
|
|
362
|
+
))}
|
|
363
|
+
{anchored.tail.length === 0 ? null : (
|
|
364
|
+
<CommentStrip bleed>
|
|
365
|
+
{anchored.tail.map((a): ReactNode => a.node)}
|
|
366
|
+
</CommentStrip>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</Panel>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/** Field look shared by the comment form's textarea and name input. */
|
|
375
|
+
const FORM_CONTROL_CLS = `w-full min-w-0 rounded-lg border ${CHIP_BORDER_CLS} bg-transparent px-2.5 py-1.5 text-sm outline-none transition-colors placeholder:text-neutral-400 focus-visible:border-neutral-400 focus-visible:ring-3 focus-visible:ring-neutral-200/70 dark:placeholder:text-neutral-500 dark:focus-visible:ring-neutral-800`;
|
|
376
|
+
|
|
377
|
+
const FORM_SUBMIT_CLS = `inline-flex cursor-pointer items-center rounded-md border border-transparent bg-neutral-900 px-2.5 py-1 text-xs font-medium text-white transition-colors hover:bg-neutral-700 active:translate-y-px dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-300`;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Inert markup the client clones for interactive bits — `data-comment-tpl`
|
|
381
|
+
* names each fragment: `card` is the CommentCard skeleton with `data-cc-*`
|
|
382
|
+
* fill slots, `form` the reply/new-comment form, `strip` a thread row (its
|
|
383
|
+
* `bleed` variant follows the subject: code strips stretch across the px-4
|
|
384
|
+
* code padding, diff strips don't). Templates keep this markup out of the
|
|
385
|
+
* serializer's `[data-mdxr-comment]` scan and single-source the classes.
|
|
386
|
+
*/
|
|
387
|
+
const CommentTemplates = ({ bleed }: { bleed: boolean }): ReactElement => (
|
|
388
|
+
<>
|
|
389
|
+
<template data-comment-tpl="card">
|
|
390
|
+
<div className="flex gap-2.5" data-mdxr-comment="">
|
|
391
|
+
<span
|
|
392
|
+
aria-hidden
|
|
393
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-neutral-500 dark:bg-neutral-700 dark:text-neutral-400"
|
|
394
|
+
data-cc-avatar-anon=""
|
|
395
|
+
>
|
|
396
|
+
<Icon className="h-3.5 w-3.5" name="lucide:message-square-text" />
|
|
397
|
+
</span>
|
|
398
|
+
<span
|
|
399
|
+
aria-hidden
|
|
400
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-300 text-[10px] font-semibold text-neutral-700 dark:bg-neutral-600 dark:text-neutral-100"
|
|
401
|
+
data-cc-avatar=""
|
|
402
|
+
/>
|
|
403
|
+
<article
|
|
404
|
+
className={`min-w-0 flex-1 overflow-hidden rounded-md border ${BORDER_CLS} bg-white dark:bg-neutral-950`}
|
|
405
|
+
>
|
|
406
|
+
<header
|
|
407
|
+
className={`flex flex-wrap items-center gap-x-1.5 gap-y-1 border-b ${BORDER_CLS} ${SURFACE_CLS} px-3 py-1.5 text-xs`}
|
|
408
|
+
>
|
|
409
|
+
<span className={`font-semibold ${TEXT.strong}`} data-cc-name="" />
|
|
410
|
+
<span className={TEXT.muted}>commented</span>
|
|
411
|
+
<span className="ml-auto inline-flex items-center gap-1.5">
|
|
412
|
+
<span className={`${MINI_CHIP_CLS} ${TONE.red}`} data-cc-old="">
|
|
413
|
+
old
|
|
414
|
+
</span>
|
|
415
|
+
<span className={LOC_CLS} data-cc-lines="" />
|
|
416
|
+
</span>
|
|
417
|
+
</header>
|
|
418
|
+
<div
|
|
419
|
+
className={`${TRIM_CLS} px-3.5 py-2.5 text-sm`}
|
|
420
|
+
data-cc-body=""
|
|
421
|
+
/>
|
|
422
|
+
</article>
|
|
423
|
+
</div>
|
|
424
|
+
</template>
|
|
425
|
+
<template data-comment-tpl="form">
|
|
426
|
+
<div
|
|
427
|
+
className={`rounded-md border ${BORDER_CLS} bg-white p-2.5 dark:bg-neutral-950`}
|
|
428
|
+
data-comment-form=""
|
|
429
|
+
>
|
|
430
|
+
<textarea
|
|
431
|
+
aria-label="Comment text"
|
|
432
|
+
className={FORM_CONTROL_CLS}
|
|
433
|
+
data-comment-input=""
|
|
434
|
+
placeholder="Write a comment"
|
|
435
|
+
rows={3}
|
|
436
|
+
/>
|
|
437
|
+
<div className="mt-2 flex items-center gap-2">
|
|
438
|
+
<input
|
|
439
|
+
aria-label="Name (optional)"
|
|
440
|
+
className={`${FORM_CONTROL_CLS} w-40`}
|
|
441
|
+
data-comment-author=""
|
|
442
|
+
placeholder="Name (optional)"
|
|
443
|
+
type="text"
|
|
444
|
+
/>
|
|
445
|
+
<span className="ml-auto flex items-center gap-1.5">
|
|
446
|
+
<button
|
|
447
|
+
className={ACTION_BUTTON_CLS}
|
|
448
|
+
data-comment-cancel=""
|
|
449
|
+
type="button"
|
|
450
|
+
>
|
|
451
|
+
Cancel
|
|
452
|
+
</button>
|
|
453
|
+
<button
|
|
454
|
+
className={FORM_SUBMIT_CLS}
|
|
455
|
+
data-comment-submit=""
|
|
456
|
+
type="button"
|
|
457
|
+
>
|
|
458
|
+
Comment
|
|
459
|
+
</button>
|
|
460
|
+
</span>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</template>
|
|
464
|
+
<template data-comment-tpl="strip">
|
|
465
|
+
<div className={commentStripCls(bleed)} data-comment-strip="">
|
|
466
|
+
<div className="mdxr-thread-tools" data-thread-tools="">
|
|
467
|
+
<CommentReplyButton />
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</template>
|
|
471
|
+
</>
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
export const Comments = defineComponent(
|
|
475
|
+
{
|
|
476
|
+
description:
|
|
477
|
+
'コード/diff フェンスへの行アンカーコメント(GitHub レビュー形式)。最初のフェンス子が対象。<Comment> 子は lines="40"|"40-52"|"40-" で範囲指定 — diff では side="old|new"(左右)と file=(複数ファイル時の対象選択)も解釈。author/severity/title でヘッダ装飾。lines 省略やアンカー不能なコメントは末尾にファイルレベルコメントとして表示。読者は行ホバーの + でコメント追加、各スレッドの Reply で返信でき、「Copy markdown」がコード+全コメントを <Comments> マークアップとしてコピーする(貼り戻せば永続化できる)',
|
|
478
|
+
schema: v.looseObject({}),
|
|
479
|
+
},
|
|
480
|
+
({ children }) => {
|
|
481
|
+
const flat = flattenChildren(children);
|
|
482
|
+
const subject = flat.find((n) => isEl(n, Pre));
|
|
483
|
+
if (subject === undefined) {
|
|
484
|
+
// No fence to anchor to — comments still render as standalone cards.
|
|
485
|
+
return (
|
|
486
|
+
<div className="not-prose space-y-3">{indexChildren(children)}</div>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const block = subjectBlock(subject);
|
|
490
|
+
const annotations = flat
|
|
491
|
+
.filter((n) => isEl(n, Comment))
|
|
492
|
+
.map((el, i) => toAnnotation(el, i, block.filename));
|
|
493
|
+
const rest = flat.filter((n) => n !== subject && !isEl(n, Comment));
|
|
494
|
+
const diff = isDiffBlock(block);
|
|
495
|
+
const view = diff ? (
|
|
496
|
+
<DiffView
|
|
497
|
+
comments={annotations.map((a): DiffCommentSpec => ({
|
|
498
|
+
file: a.file,
|
|
499
|
+
lines: a.lines,
|
|
500
|
+
node: a.node,
|
|
501
|
+
range: a.range,
|
|
502
|
+
side: a.side,
|
|
503
|
+
}))}
|
|
504
|
+
filename={block.filename}
|
|
505
|
+
hl={parseDiffHl(block.diffhl)}
|
|
506
|
+
text={block.text}
|
|
507
|
+
/>
|
|
508
|
+
) : (
|
|
509
|
+
<AnnotatedCode block={block} comments={annotations} />
|
|
510
|
+
);
|
|
511
|
+
// data-comments* feeds the markdown serializer (doc-events.ts): the
|
|
512
|
+
// fence round-trips verbatim, cards carry their own <Comment> attrs.
|
|
513
|
+
return (
|
|
514
|
+
<div
|
|
515
|
+
data-comments=""
|
|
516
|
+
data-comments-code={block.text}
|
|
517
|
+
data-comments-lang={block.lang}
|
|
518
|
+
data-comments-meta={nonEmpty(block.meta) ? block.meta : undefined}
|
|
519
|
+
>
|
|
520
|
+
{view}
|
|
521
|
+
{rest.map((n, i) => (
|
|
522
|
+
<Fragment key={i}>{n}</Fragment>
|
|
523
|
+
))}
|
|
524
|
+
<div className="not-prose mt-2 flex items-center justify-between gap-3">
|
|
525
|
+
<span className={`text-xs ${TEXT.faint}`}>
|
|
526
|
+
Hover a line or hit Reply to comment, then copy the markup.
|
|
527
|
+
</span>
|
|
528
|
+
<button
|
|
529
|
+
className={ACTION_BUTTON_CLS}
|
|
530
|
+
data-comments-copy=""
|
|
531
|
+
type="button"
|
|
532
|
+
>
|
|
533
|
+
<CopyFeedback
|
|
534
|
+
done="Copied"
|
|
535
|
+
icon="lucide:clipboard-list"
|
|
536
|
+
label="Copy markdown"
|
|
537
|
+
/>
|
|
538
|
+
</button>
|
|
539
|
+
</div>
|
|
540
|
+
<CommentTemplates bleed={!diff} />
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode, ReactElement } from "react";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
import { defineComponent } from "../define.js";
|
|
5
|
+
import { TITLE_PROP } from "./attrs.js";
|
|
6
|
+
import { Icon } from "./icon.js";
|
|
7
|
+
import { TONE_BAND, TONE_BORDER, TRIM_CLS } from "./tones.js";
|
|
8
|
+
|
|
9
|
+
const panel = (
|
|
10
|
+
cls: { border: string; head: string },
|
|
11
|
+
icon: string,
|
|
12
|
+
label: string,
|
|
13
|
+
title: string | undefined,
|
|
14
|
+
children: ReactNode
|
|
15
|
+
): ReactElement => (
|
|
16
|
+
<div className={`overflow-hidden rounded-lg border ${cls.border}`}>
|
|
17
|
+
<div
|
|
18
|
+
className={`flex items-center gap-1.5 border-b px-4 py-2 text-xs font-semibold ${cls.head}`}
|
|
19
|
+
>
|
|
20
|
+
<Icon className="h-3.5 w-3.5" name={icon} />
|
|
21
|
+
{title ?? label}
|
|
22
|
+
</div>
|
|
23
|
+
<div className={`px-4 py-2.5 ${TRIM_CLS}`}>{children}</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Before/After panels — wrap in `<Columns>` for side-by-side layout.
|
|
29
|
+
* `title` overrides the label (e.g. "Current" / "Proposed").
|
|
30
|
+
*/
|
|
31
|
+
export const Before = defineComponent(
|
|
32
|
+
{
|
|
33
|
+
description:
|
|
34
|
+
"Before パネル (赤系)。<Columns> と組み合わせて before/after 比較に。title でラベル上書き可",
|
|
35
|
+
schema: v.looseObject(TITLE_PROP),
|
|
36
|
+
},
|
|
37
|
+
({ title, children }) =>
|
|
38
|
+
panel(
|
|
39
|
+
{
|
|
40
|
+
border: TONE_BORDER.red,
|
|
41
|
+
head: `${TONE_BORDER.red} ${TONE_BAND.red}`,
|
|
42
|
+
},
|
|
43
|
+
"lucide:history",
|
|
44
|
+
"Before",
|
|
45
|
+
title,
|
|
46
|
+
children
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const After = defineComponent(
|
|
51
|
+
{
|
|
52
|
+
description:
|
|
53
|
+
"After パネル (緑系)。<Columns> と組み合わせて before/after 比較に。title でラベル上書き可",
|
|
54
|
+
schema: v.looseObject(TITLE_PROP),
|
|
55
|
+
},
|
|
56
|
+
({ title, children }) =>
|
|
57
|
+
panel(
|
|
58
|
+
{
|
|
59
|
+
border: TONE_BORDER.emerald,
|
|
60
|
+
head: `${TONE_BORDER.emerald} ${TONE_BAND.emerald}`,
|
|
61
|
+
},
|
|
62
|
+
"lucide:sparkles",
|
|
63
|
+
"After",
|
|
64
|
+
title,
|
|
65
|
+
children
|
|
66
|
+
)
|
|
67
|
+
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
import { defineComponent } from "../define.js";
|
|
4
|
+
import { nonEmpty } from "../guards.js";
|
|
5
|
+
import { Pill, TrimBody } from "./bits.js";
|
|
6
|
+
import { TEXT, TONE, TONE_BORDER } from "./tones.js";
|
|
7
|
+
|
|
8
|
+
export const DECISION_STATUSES = [
|
|
9
|
+
"accepted",
|
|
10
|
+
"deprecated",
|
|
11
|
+
"proposed",
|
|
12
|
+
"rejected",
|
|
13
|
+
"superseded",
|
|
14
|
+
] as const;
|
|
15
|
+
export type DecisionStatus = (typeof DECISION_STATUSES)[number];
|
|
16
|
+
|
|
17
|
+
const STYLES: Record<
|
|
18
|
+
DecisionStatus,
|
|
19
|
+
{ cls: string; icon: string; label: string }
|
|
20
|
+
> = {
|
|
21
|
+
accepted: {
|
|
22
|
+
cls: TONE.emerald,
|
|
23
|
+
icon: "lucide:check",
|
|
24
|
+
label: "Accepted",
|
|
25
|
+
},
|
|
26
|
+
deprecated: {
|
|
27
|
+
cls: TONE.neutral,
|
|
28
|
+
icon: "lucide:archive",
|
|
29
|
+
label: "Deprecated",
|
|
30
|
+
},
|
|
31
|
+
proposed: {
|
|
32
|
+
cls: TONE.sky,
|
|
33
|
+
icon: "lucide:pen-line",
|
|
34
|
+
label: "Proposed",
|
|
35
|
+
},
|
|
36
|
+
rejected: {
|
|
37
|
+
cls: TONE.red,
|
|
38
|
+
icon: "lucide:x",
|
|
39
|
+
label: "Rejected",
|
|
40
|
+
},
|
|
41
|
+
superseded: {
|
|
42
|
+
cls: TONE.amber,
|
|
43
|
+
icon: "lucide:replace",
|
|
44
|
+
label: "Superseded",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const Decision = defineComponent(
|
|
49
|
+
{
|
|
50
|
+
description:
|
|
51
|
+
"決定記録 (ADR-lite)。status は proposed|accepted|rejected|deprecated|superseded。:::decision は軽量版の Callout",
|
|
52
|
+
schema: v.looseObject({
|
|
53
|
+
date: v.optional(v.string()),
|
|
54
|
+
status: v.optional(v.picklist(DECISION_STATUSES), "proposed"),
|
|
55
|
+
title: v.string(),
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
({ title, status, date, children }) => {
|
|
59
|
+
const s = STYLES[status];
|
|
60
|
+
return (
|
|
61
|
+
<section
|
|
62
|
+
className={`my-6 rounded-lg border bg-indigo-50/50 px-4 py-3 dark:bg-indigo-950/30 ${TONE_BORDER.indigo}`}
|
|
63
|
+
>
|
|
64
|
+
<div className="flex flex-wrap items-baseline gap-2">
|
|
65
|
+
<span className="font-semibold text-indigo-900 dark:text-indigo-100">
|
|
66
|
+
{title}
|
|
67
|
+
</span>
|
|
68
|
+
<Pill className={s.cls} icon={s.icon}>
|
|
69
|
+
{s.label}
|
|
70
|
+
</Pill>
|
|
71
|
+
{nonEmpty(date) ? (
|
|
72
|
+
<time className={`font-mono text-xs ${TEXT.muted}`}>{date}</time>
|
|
73
|
+
) : null}
|
|
74
|
+
</div>
|
|
75
|
+
<TrimBody className="mt-1.5 text-sm">{children}</TrimBody>
|
|
76
|
+
</section>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
);
|