@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/ask.tsx
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { isValidElement } from "react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
|
|
5
|
+
import { formatAnswerSheet } from "../ask-sheet.js";
|
|
6
|
+
import type { SheetEntry } from "../ask-sheet.js";
|
|
7
|
+
import type { DocProps } from "../define.js";
|
|
8
|
+
import { defineComponent, flattenChildren, textOf } from "../define.js";
|
|
9
|
+
import { nonEmpty } from "../guards.js";
|
|
10
|
+
import { Choice, Question, QUESTION_TYPES } from "./ask-question.js";
|
|
11
|
+
import type { QuestionType } from "./ask-question.js";
|
|
12
|
+
import { attrTrue } from "./attrs.js";
|
|
13
|
+
import {
|
|
14
|
+
ACTION_BUTTON_CLS,
|
|
15
|
+
CAPTION_CLS,
|
|
16
|
+
CopyFeedback,
|
|
17
|
+
PANEL_CLS,
|
|
18
|
+
} from "./bits.js";
|
|
19
|
+
import { isEl, propOf } from "./children.js";
|
|
20
|
+
import { Icon } from "./icon.js";
|
|
21
|
+
import { BORDER_CLS, DIVIDE_CLS, SURFACE_CLS, TEXT } from "./tones.js";
|
|
22
|
+
|
|
23
|
+
export { Choice, Question, QUESTION_TYPES } from "./ask-question.js";
|
|
24
|
+
export type { QuestionType } from "./ask-question.js";
|
|
25
|
+
|
|
26
|
+
type QuestionEl = ReactElement<DocProps>;
|
|
27
|
+
|
|
28
|
+
// The initial sheet must match what client/doc-events.ts would compute —
|
|
29
|
+
// syncAsk rewrites [data-ask-output] before hydration, so SSR renders the
|
|
30
|
+
// seeded sheet rather than a placeholder; otherwise hydrateRoot would patch
|
|
31
|
+
// the pane back and revert the reader's default answers.
|
|
32
|
+
|
|
33
|
+
/** choiceText: a <Choice>'s visible label text, falling back to `value`. */
|
|
34
|
+
const choiceLabel = (c: QuestionEl): string => {
|
|
35
|
+
const text = textOf(propOf(c, "children")).replaceAll(/\s+/gu, " ").trim();
|
|
36
|
+
if (text !== "") {
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
const value = propOf(c, "value");
|
|
40
|
+
return typeof value === "string" ? value : "";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const choicesOf = (q: QuestionEl): QuestionEl[] =>
|
|
44
|
+
flattenChildren(q.props.children).filter((c): c is QuestionEl =>
|
|
45
|
+
isEl(c, Choice)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const checkedChoicesOf = (q: QuestionEl): QuestionEl[] =>
|
|
49
|
+
choicesOf(q).filter((c) => attrTrue(propOf(c, "checked")));
|
|
50
|
+
|
|
51
|
+
// Same defaulting as Question's render: explicit `type`, else `choice` when
|
|
52
|
+
// <Choice> children exist, else `text`.
|
|
53
|
+
const questionType = (q: QuestionEl): QuestionType =>
|
|
54
|
+
QUESTION_TYPES.find((t) => t === propOf(q, "type")) ??
|
|
55
|
+
(choicesOf(q).length > 0 ? "choice" : "text");
|
|
56
|
+
|
|
57
|
+
const selectAnswer = (q: QuestionEl): string => {
|
|
58
|
+
const [checked] = checkedChoicesOf(q);
|
|
59
|
+
// <select defaultValue> only lands when the checked choice's `value` is
|
|
60
|
+
// non-empty; otherwise the browser stays on the first option — the
|
|
61
|
+
// placeholder (value "", i.e. unanswered) when present, else the first
|
|
62
|
+
// choice. The client reads s.value, so a picked option with value=""
|
|
63
|
+
// still reports "".
|
|
64
|
+
const sel =
|
|
65
|
+
checked !== undefined && nonEmpty(propOf(checked, "value"))
|
|
66
|
+
? checked
|
|
67
|
+
: undefined;
|
|
68
|
+
const picked =
|
|
69
|
+
sel ?? (nonEmpty(propOf(q, "placeholder")) ? undefined : choicesOf(q)[0]);
|
|
70
|
+
if (picked === undefined || !nonEmpty(propOf(picked, "value"))) {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
return choiceLabel(picked);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// answerOf for the untouched default state: checked choices read as their
|
|
77
|
+
// visible text, a select as its first selected option, a toggle as yes/no,
|
|
78
|
+
// free-form fields as their `value`.
|
|
79
|
+
const defaultAnswer = (q: QuestionEl): string => {
|
|
80
|
+
const t = questionType(q);
|
|
81
|
+
if (t === "multi") {
|
|
82
|
+
return checkedChoicesOf(q)
|
|
83
|
+
.map(choiceLabel)
|
|
84
|
+
.filter((s) => s !== "")
|
|
85
|
+
.join(", ");
|
|
86
|
+
}
|
|
87
|
+
if (t === "choice") {
|
|
88
|
+
const [first] = checkedChoicesOf(q);
|
|
89
|
+
return first === undefined ? "" : choiceLabel(first);
|
|
90
|
+
}
|
|
91
|
+
if (t === "select") {
|
|
92
|
+
return selectAnswer(q);
|
|
93
|
+
}
|
|
94
|
+
if (t === "toggle") {
|
|
95
|
+
return attrTrue(propOf(q, "checked")) ? "yes" : "no";
|
|
96
|
+
}
|
|
97
|
+
const value = propOf(q, "value");
|
|
98
|
+
return typeof value === "string" ? value.trim() : "";
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Every <Question> under <Ask>, matching the client's
|
|
102
|
+
// querySelectorAll("[data-mdxr-q]") reach — descend through element children
|
|
103
|
+
// (a Question's own children are choices, never questions).
|
|
104
|
+
const collectQuestions = (node: ReactNode, out: QuestionEl[]): void => {
|
|
105
|
+
for (const c of flattenChildren(node)) {
|
|
106
|
+
if (isEl<DocProps>(c, Question)) {
|
|
107
|
+
out.push(c);
|
|
108
|
+
} else if (isValidElement<DocProps>(c)) {
|
|
109
|
+
collectQuestions(c.props.children, out);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// askMarkdown for the initial DOM — the client's formatter (ask-sheet.ts)
|
|
115
|
+
// applied to the untouched default answers.
|
|
116
|
+
const answerSheet = (title: unknown, children: ReactNode): string => {
|
|
117
|
+
const questions: QuestionEl[] = [];
|
|
118
|
+
collectQuestions(children, questions);
|
|
119
|
+
const entries: SheetEntry[] = [];
|
|
120
|
+
for (const q of questions) {
|
|
121
|
+
const rawLabel = nonEmpty(propOf(q, "label"))
|
|
122
|
+
? propOf(q, "label")
|
|
123
|
+
: propOf(q, "name");
|
|
124
|
+
entries.push({
|
|
125
|
+
answer: defaultAnswer(q),
|
|
126
|
+
label: typeof rawLabel === "string" ? rawLabel : "",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return formatAnswerSheet(
|
|
130
|
+
typeof title === "string" ? title : undefined,
|
|
131
|
+
entries
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A block of questions asking the reader for input — e.g. open decisions in
|
|
137
|
+
* a plan. Built on native form controls so it is interactive in static HTML.
|
|
138
|
+
* Answers are rendered live as a Markdown sheet (`- **label**: answer` lines
|
|
139
|
+
* under `# title`) in the output pane; "Copy answers" copies it to the
|
|
140
|
+
* clipboard and "Save .md" downloads it (nothing is submitted anywhere).
|
|
141
|
+
* `Choice`/`Question` and the per-type field chrome live in
|
|
142
|
+
* `ask-question.tsx`.
|
|
143
|
+
*/
|
|
144
|
+
export const Ask = defineComponent(
|
|
145
|
+
{
|
|
146
|
+
description:
|
|
147
|
+
"ユーザーへの質問フォーム。<Question> を並べる。ネイティブコントロールで JS なしに操作可能。回答はラベル付きの Markdown として表示され、「Copy answers」でコピー、「Save .md」で保存できる",
|
|
148
|
+
schema: v.looseObject({
|
|
149
|
+
description: v.optional(v.string()),
|
|
150
|
+
title: v.optional(v.string()),
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
({ title, description, children }) => (
|
|
154
|
+
<section
|
|
155
|
+
className={`mdxr-ask ${PANEL_CLS}`}
|
|
156
|
+
data-ask
|
|
157
|
+
data-ask-title={nonEmpty(title) ? title : undefined}
|
|
158
|
+
>
|
|
159
|
+
<div className={CAPTION_CLS}>
|
|
160
|
+
<div className="flex items-center gap-1.5 font-semibold">
|
|
161
|
+
<Icon className="h-3.5 w-3.5" name="lucide:list-checks" />
|
|
162
|
+
{nonEmpty(title) ? title : "Questions"}
|
|
163
|
+
</div>
|
|
164
|
+
{nonEmpty(description) ? <p className="mt-1">{description}</p> : null}
|
|
165
|
+
</div>
|
|
166
|
+
<div className={DIVIDE_CLS}>{children}</div>
|
|
167
|
+
<div className={`border-t ${BORDER_CLS}`}>
|
|
168
|
+
<div
|
|
169
|
+
className={`flex items-center gap-1.5 px-4 pt-2.5 text-xs font-medium ${TEXT.faint}`}
|
|
170
|
+
>
|
|
171
|
+
<Icon className="h-3 w-3" name="lucide:file-text" />
|
|
172
|
+
Markdown
|
|
173
|
+
</div>
|
|
174
|
+
<pre
|
|
175
|
+
className={`m-0 max-h-64 overflow-auto px-4 pt-1 pb-3 font-mono text-xs leading-relaxed break-words whitespace-pre-wrap ${TEXT.body}`}
|
|
176
|
+
data-ask-output
|
|
177
|
+
>
|
|
178
|
+
{answerSheet(title, children)}
|
|
179
|
+
</pre>
|
|
180
|
+
</div>
|
|
181
|
+
<div
|
|
182
|
+
className={`flex items-center justify-between gap-3 border-t ${SURFACE_CLS} px-4 py-2 ${BORDER_CLS}`}
|
|
183
|
+
>
|
|
184
|
+
<span className={`text-xs ${TEXT.faint}`}>
|
|
185
|
+
Updates live as you answer — copy or save it.
|
|
186
|
+
</span>
|
|
187
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
188
|
+
<button className={ACTION_BUTTON_CLS} data-ask-copy type="button">
|
|
189
|
+
<CopyFeedback
|
|
190
|
+
done="Copied"
|
|
191
|
+
icon="lucide:clipboard-list"
|
|
192
|
+
label="Copy answers"
|
|
193
|
+
/>
|
|
194
|
+
</button>
|
|
195
|
+
<button className={ACTION_BUTTON_CLS} data-ask-save type="button">
|
|
196
|
+
<CopyFeedback
|
|
197
|
+
done="Saved"
|
|
198
|
+
icon="lucide:download"
|
|
199
|
+
label="Save .md"
|
|
200
|
+
/>
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</section>
|
|
205
|
+
)
|
|
206
|
+
);
|
package/src/ui/attrs.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MDX attributes arrive as strings: `open` → `""`, `open="false"` →
|
|
5
|
+
* `"false"`. `attrTrue` treats presence/`"true"` as on; `attrFalse` treats
|
|
6
|
+
* only an explicit `false`/`"false"` as off (absent stays open).
|
|
7
|
+
*/
|
|
8
|
+
export const attrTrue = (x: unknown): boolean =>
|
|
9
|
+
x === true || x === "" || x === "true";
|
|
10
|
+
|
|
11
|
+
export const attrFalse = (x: unknown): boolean => x === false || x === "false";
|
|
12
|
+
|
|
13
|
+
/** Numeric part of `"40"`, `"-15"`, `"40%"`, `"120ms"`, `1.2` — suffix text is dropped. */
|
|
14
|
+
export const numOf = (x: unknown): number | undefined => {
|
|
15
|
+
if (typeof x === "number") {
|
|
16
|
+
return Number.isFinite(x) ? x : undefined;
|
|
17
|
+
}
|
|
18
|
+
if (typeof x !== "string") {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const m = /^\s*(?<n>[+-]?\d+(?:\.\d+)?)/u.exec(x);
|
|
22
|
+
return m?.groups === undefined ? undefined : Number(m.groups.n);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional boolean-ish prop schema: accepts a JSX boolean or an MDX string
|
|
27
|
+
* attribute; read it back with `attrTrue`/`attrFalse`.
|
|
28
|
+
*/
|
|
29
|
+
export const BOOLISH_PROP = v.optional(v.union([v.boolean(), v.string()]));
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* String-or-number prop atom: MDX attributes arrive as strings but JSX
|
|
33
|
+
* expressions like `duration={300}` produce real numbers — both are valid.
|
|
34
|
+
* Required sites use it bare (`id: NUMISH`); optional sites wrap it.
|
|
35
|
+
*/
|
|
36
|
+
export const NUMISH = v.union([v.string(), v.number()]);
|
|
37
|
+
|
|
38
|
+
/** Optional `title` — the heading attribute most containers share. */
|
|
39
|
+
export const TITLE_PROP = { title: v.optional(v.string()) } as const;
|
|
40
|
+
|
|
41
|
+
/** `href` + `lines` — the link-override pair shared by file-referencing props. */
|
|
42
|
+
export const LINK_LINES_PROPS = {
|
|
43
|
+
href: v.optional(v.string()),
|
|
44
|
+
lines: v.optional(v.string()),
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
/** Optional `href`/`lines`/`path` — the full file-link attribute triple. */
|
|
48
|
+
export const FILE_LINK_PROPS = {
|
|
49
|
+
...LINK_LINES_PROPS,
|
|
50
|
+
path: v.optional(v.string()),
|
|
51
|
+
} as const;
|
package/src/ui/audit.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
import { defineComponent } from "../define.js";
|
|
4
|
+
import { nonEmpty, safeHref } from "../guards.js";
|
|
5
|
+
import { attrTrue, BOOLISH_PROP } from "./attrs.js";
|
|
6
|
+
import {
|
|
7
|
+
CaptionBar,
|
|
8
|
+
ListPanel,
|
|
9
|
+
ListRow,
|
|
10
|
+
MaybeLink,
|
|
11
|
+
Pill,
|
|
12
|
+
RowNote,
|
|
13
|
+
} from "./bits.js";
|
|
14
|
+
import { countByProp } from "./children.js";
|
|
15
|
+
import { Icon } from "./icon.js";
|
|
16
|
+
import { isSeverity, SEVERITY_LEVELS, SEVERITY_STYLES } from "./severity.js";
|
|
17
|
+
import {
|
|
18
|
+
CAPTION_TITLE_CLS,
|
|
19
|
+
COUNT_CHIP_CLS,
|
|
20
|
+
LOC_CLS,
|
|
21
|
+
MONO_CLS,
|
|
22
|
+
TEXT,
|
|
23
|
+
TRIM_CLS,
|
|
24
|
+
} from "./tones.js";
|
|
25
|
+
|
|
26
|
+
export const Vuln = defineComponent(
|
|
27
|
+
{
|
|
28
|
+
description:
|
|
29
|
+
"脆弱性1件。severity は critical|high|medium|low|info、id に CVE/GHSA 番号、package に対象パッケージ、affected に影響範囲、fix に修正バージョン、href でアドバイザリリンク、wontfix で対応外マーク。children は説明文",
|
|
30
|
+
schema: v.looseObject({
|
|
31
|
+
affected: v.optional(v.string()),
|
|
32
|
+
fix: v.optional(v.string()),
|
|
33
|
+
href: v.optional(v.string()),
|
|
34
|
+
id: v.optional(v.string()),
|
|
35
|
+
package: v.optional(v.string()),
|
|
36
|
+
severity: v.optional(v.picklist(SEVERITY_LEVELS), "medium"),
|
|
37
|
+
title: v.optional(v.string()),
|
|
38
|
+
wontfix: BOOLISH_PROP,
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
({
|
|
42
|
+
severity,
|
|
43
|
+
id,
|
|
44
|
+
package: pkg,
|
|
45
|
+
affected,
|
|
46
|
+
fix,
|
|
47
|
+
href,
|
|
48
|
+
title,
|
|
49
|
+
wontfix,
|
|
50
|
+
children,
|
|
51
|
+
}) => {
|
|
52
|
+
const s = SEVERITY_STYLES[severity];
|
|
53
|
+
const link = safeHref(href);
|
|
54
|
+
return (
|
|
55
|
+
<ListRow>
|
|
56
|
+
<Pill className={s.chip} icon={s.icon}>
|
|
57
|
+
{s.label}
|
|
58
|
+
</Pill>
|
|
59
|
+
<span className="min-w-0">
|
|
60
|
+
{nonEmpty(title) ? (
|
|
61
|
+
<span className="text-sm font-medium">{title}</span>
|
|
62
|
+
) : null}
|
|
63
|
+
{nonEmpty(id) ? (
|
|
64
|
+
<MaybeLink
|
|
65
|
+
className={`${LOC_CLS} no-underline hover:underline ${nonEmpty(title) ? "ml-2" : ""}`}
|
|
66
|
+
href={link}
|
|
67
|
+
>
|
|
68
|
+
{id}
|
|
69
|
+
</MaybeLink>
|
|
70
|
+
) : null}
|
|
71
|
+
</span>
|
|
72
|
+
{nonEmpty(pkg) ? <code className={MONO_CLS}>{pkg}</code> : null}
|
|
73
|
+
{nonEmpty(affected) ? (
|
|
74
|
+
<code className={`${MONO_CLS} ${TEXT.muted}`}>{affected}</code>
|
|
75
|
+
) : null}
|
|
76
|
+
{nonEmpty(fix) ? (
|
|
77
|
+
<span
|
|
78
|
+
className={`inline-flex shrink-0 items-center gap-1 font-mono text-xs ${TEXT.muted}`}
|
|
79
|
+
>
|
|
80
|
+
<Icon className="h-3 w-3" name="lucide:arrow-right" />
|
|
81
|
+
<span className="font-medium text-emerald-600 dark:text-emerald-400">
|
|
82
|
+
{fix}
|
|
83
|
+
</span>
|
|
84
|
+
</span>
|
|
85
|
+
) : null}
|
|
86
|
+
{attrTrue(wontfix) ? (
|
|
87
|
+
<span className={`${COUNT_CHIP_CLS} uppercase`}>wontfix</span>
|
|
88
|
+
) : null}
|
|
89
|
+
<RowNote className={TRIM_CLS}>{children}</RowNote>
|
|
90
|
+
</ListRow>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const Audit = defineComponent(
|
|
96
|
+
{
|
|
97
|
+
description:
|
|
98
|
+
"脆弱性・監査結果のコンテナ。<Vuln> を並べる。title はキャプション、tool はスキャナ名チップ (npm audit, osv, trivy …)。severity 別の件数を自動集計",
|
|
99
|
+
schema: v.looseObject({
|
|
100
|
+
title: v.optional(v.string()),
|
|
101
|
+
tool: v.optional(v.string()),
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
({ title, tool, children }) => {
|
|
105
|
+
const counts = countByProp(
|
|
106
|
+
children,
|
|
107
|
+
Vuln,
|
|
108
|
+
"severity",
|
|
109
|
+
isSeverity,
|
|
110
|
+
"medium"
|
|
111
|
+
);
|
|
112
|
+
const total = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
113
|
+
const caption = nonEmpty(title) || nonEmpty(tool) || total > 0;
|
|
114
|
+
return (
|
|
115
|
+
<ListPanel>
|
|
116
|
+
{caption ? (
|
|
117
|
+
<CaptionBar className={CAPTION_TITLE_CLS}>
|
|
118
|
+
<Icon className="h-3.5 w-3.5" name="lucide:shield-alert" />
|
|
119
|
+
{nonEmpty(title) ? title : "Audit"}
|
|
120
|
+
{nonEmpty(tool) ? (
|
|
121
|
+
<span className={COUNT_CHIP_CLS}>{tool}</span>
|
|
122
|
+
) : null}
|
|
123
|
+
<span className="ml-auto flex items-center gap-x-2 font-medium">
|
|
124
|
+
{SEVERITY_LEVELS.map((k) => {
|
|
125
|
+
const c = counts.get(k);
|
|
126
|
+
return c === undefined ? null : (
|
|
127
|
+
<Pill className={SEVERITY_STYLES[k].chip} key={k}>
|
|
128
|
+
{c} {SEVERITY_STYLES[k].label.toLowerCase()}
|
|
129
|
+
</Pill>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</span>
|
|
133
|
+
</CaptionBar>
|
|
134
|
+
) : null}
|
|
135
|
+
{children}
|
|
136
|
+
</ListPanel>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
);
|